Authentication Example
This guide demonstrates how to add authentication to your @adi-family/http API using JWT tokens, including protected routes, user login, and middleware patterns.
Overview
We'll implement:
- User registration and login
- JWT token generation and validation
- Protected routes requiring authentication
- Authorization based on user roles
- Client-side token management
1. Setup Dependencies
bash
npm install jsonwebtoken bcryptjs
npm install -D @types/jsonwebtoken @types/bcryptjs2. Auth Contracts
Define authentication-related API contracts:
typescript
// contracts/auth.ts
import { route } from '@adi-family/http'
import { z } from 'zod'
import type { HandlerConfig } from '@adi-family/http'
// Register new user
export const registerConfig = {
method: 'POST',
route: route.static('/api/auth/register'),
body: {
schema: z.object({
email: z.string().email(),
password: z.string().min(8).max(100),
name: z.string().min(1).max(255)
})
},
response: {
schema: z.object({
id: z.string(),
email: z.string(),
name: z.string(),
token: z.string()
})
}
} as const satisfies HandlerConfig
// Login
export const loginConfig = {
method: 'POST',
route: route.static('/api/auth/login'),
body: {
schema: z.object({
email: z.string().email(),
password: z.string()
})
},
response: {
schema: z.object({
id: z.string(),
email: z.string(),
name: z.string(),
token: z.string()
})
}
} as const satisfies HandlerConfig
// Get current user (protected)
export const getMeConfig = {
method: 'GET',
route: route.static('/api/auth/me'),
response: {
schema: z.object({
id: z.string(),
email: z.string(),
name: z.string(),
role: z.enum(['admin', 'user'])
})
}
} as const satisfies HandlerConfig
// Refresh token
export const refreshTokenConfig = {
method: 'POST',
route: route.static('/api/auth/refresh'),
response: {
schema: z.object({
token: z.string()
})
}
} as const satisfies HandlerConfig3. Protected Resource Contracts
typescript
// contracts/projects.ts
import { route } from '@adi-family/http'
import { z } from 'zod'
import type { HandlerConfig } from '@adi-family/http'
// Get user's projects (protected)
export const getMyProjectsConfig = {
method: 'GET',
route: route.static('/api/projects'),
query: {
schema: z.object({
page: z.number().min(1).default(1),
limit: z.number().min(1).max(100).default(10)
})
},
response: {
schema: z.object({
data: z.array(z.object({
id: z.string(),
name: z.string(),
ownerId: z.string()
})),
total: z.number()
})
}
} as const satisfies HandlerConfig
// Create project (protected)
export const createProjectConfig = {
method: 'POST',
route: route.static('/api/projects'),
body: {
schema: z.object({
name: z.string().min(1).max(255),
description: z.string().max(1000)
})
},
response: {
schema: z.object({
id: z.string(),
name: z.string(),
description: z.string(),
ownerId: z.string()
})
}
} as const satisfies HandlerConfig
// Delete project (protected, owner only)
export const deleteProjectConfig = {
method: 'DELETE',
route: route.dynamic('/api/projects/:id', z.object({ id: z.string() })),
response: {
schema: z.object({
success: z.boolean()
})
}
} as const satisfies HandlerConfig4. Auth Utilities
Create helper functions for authentication:
typescript
// server/auth/utils.ts
import jwt from 'jsonwebtoken'
import bcrypt from 'bcryptjs'
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production'
const JWT_EXPIRES_IN = '24h'
export interface TokenPayload {
userId: string
email: string
role: 'admin' | 'user'
}
export async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, 10)
}
export async function comparePassword(
password: string,
hash: string
): Promise<boolean> {
return bcrypt.compare(password, hash)
}
export function generateToken(payload: TokenPayload): string {
return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN })
}
export function verifyToken(token: string): TokenPayload {
try {
return jwt.verify(token, JWT_SECRET) as TokenPayload
} catch (error) {
throw new Error('Invalid or expired token')
}
}
export function extractBearerToken(authHeader: string | null): string | null {
if (!authHeader) return null
const match = authHeader.match(/^Bearer (.+)$/)
return match ? match[1] : null
}5. Auth Handlers
Implement authentication handlers:
typescript
// server/handlers/auth.ts
import { handler } from '@adi-family/http'
import {
registerConfig,
loginConfig,
getMeConfig,
refreshTokenConfig
} from '../../contracts/auth'
import {
hashPassword,
comparePassword,
generateToken,
verifyToken,
extractBearerToken
} from '../auth/utils'
// In-memory database for this example
const db = {
users: new Map<string, {
id: string
email: string
name: string
password: string
role: 'admin' | 'user'
}>()
}
// Register handler
export const registerHandler = handler(registerConfig, async (ctx) => {
const { email, password, name } = ctx.body
// Check if user already exists
const existing = Array.from(db.users.values()).find(u => u.email === email)
if (existing) {
throw new Error('Email already registered')
}
// Create user
const id = crypto.randomUUID()
const hashedPassword = await hashPassword(password)
const user = {
id,
email,
name,
password: hashedPassword,
role: 'user' as const
}
db.users.set(id, user)
// Generate token
const token = generateToken({
userId: id,
email: user.email,
role: user.role
})
return {
id: user.id,
email: user.email,
name: user.name,
token
}
})
// Login handler
export const loginHandler = handler(loginConfig, async (ctx) => {
const { email, password } = ctx.body
// Find user
const user = Array.from(db.users.values()).find(u => u.email === email)
if (!user) {
throw new Error('Invalid credentials')
}
// Verify password
const isValid = await comparePassword(password, user.password)
if (!isValid) {
throw new Error('Invalid credentials')
}
// Generate token
const token = generateToken({
userId: user.id,
email: user.email,
role: user.role
})
return {
id: user.id,
email: user.email,
name: user.name,
token
}
})
// Get current user handler (protected)
export const getMeHandler = handler(getMeConfig, async (ctx) => {
// Extract token from Authorization header
const token = extractBearerToken(ctx.headers.get('authorization'))
if (!token) {
throw new Error('Unauthorized')
}
// Verify token
const payload = verifyToken(token)
// Get user
const user = db.users.get(payload.userId)
if (!user) {
throw new Error('User not found')
}
return {
id: user.id,
email: user.email,
name: user.name,
role: user.role
}
})
// Refresh token handler
export const refreshTokenHandler = handler(refreshTokenConfig, async (ctx) => {
const token = extractBearerToken(ctx.headers.get('authorization'))
if (!token) {
throw new Error('Unauthorized')
}
// Verify old token
const payload = verifyToken(token)
// Generate new token
const newToken = generateToken({
userId: payload.userId,
email: payload.email,
role: payload.role
})
return { token: newToken }
})6. Protected Resource Handlers
typescript
// server/handlers/projects.ts
import { handler } from '@adi-family/http'
import {
getMyProjectsConfig,
createProjectConfig,
deleteProjectConfig
} from '../../contracts/projects'
import { verifyToken, extractBearerToken } from '../auth/utils'
const db = {
projects: new Map<string, {
id: string
name: string
description: string
ownerId: string
}>()
}
// Helper to get authenticated user
function getAuthenticatedUser(authHeader: string | null) {
const token = extractBearerToken(authHeader)
if (!token) {
throw new Error('Unauthorized')
}
return verifyToken(token)
}
// Get user's projects
export const getMyProjectsHandler = handler(getMyProjectsConfig, async (ctx) => {
const user = getAuthenticatedUser(ctx.headers.get('authorization'))
const userProjects = Array.from(db.projects.values())
.filter(p => p.ownerId === user.userId)
const { page, limit } = ctx.query
const start = (page - 1) * limit
const end = start + limit
return {
data: userProjects.slice(start, end),
total: userProjects.length
}
})
// Create project
export const createProjectHandler = handler(createProjectConfig, async (ctx) => {
const user = getAuthenticatedUser(ctx.headers.get('authorization'))
const id = crypto.randomUUID()
const project = {
id,
...ctx.body,
ownerId: user.userId
}
db.projects.set(id, project)
return project
})
// Delete project (owner only)
export const deleteProjectHandler = handler(deleteProjectConfig, async (ctx) => {
const user = getAuthenticatedUser(ctx.headers.get('authorization'))
const project = db.projects.get(ctx.params.id)
if (!project) {
throw new Error('Project not found')
}
// Check ownership
if (project.ownerId !== user.userId && user.role !== 'admin') {
throw new Error('Forbidden: You do not own this project')
}
db.projects.delete(ctx.params.id)
return { success: true }
})7. Express Server Setup
typescript
// server/app.ts
import express from 'express'
import { serveExpress } from '@adi-family/http-express'
import {
registerHandler,
loginHandler,
getMeHandler,
refreshTokenHandler
} from './handlers/auth'
import {
getMyProjectsHandler,
createProjectHandler,
deleteProjectHandler
} from './handlers/projects'
const app = express()
app.use(express.json())
// Register all handlers
serveExpress(app, [
// Auth routes
registerHandler,
loginHandler,
getMeHandler,
refreshTokenHandler,
// Protected routes
getMyProjectsHandler,
createProjectHandler,
deleteProjectHandler
])
// Error handling
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
console.error('Error:', err)
if (err.message === 'Unauthorized') {
return res.status(401).json({ error: 'Unauthorized' })
}
if (err.message === 'Forbidden: You do not own this project') {
return res.status(403).json({ error: err.message })
}
if (err.message === 'Invalid credentials' || err.message === 'Email already registered') {
return res.status(400).json({ error: err.message })
}
res.status(500).json({ error: 'Internal server error' })
})
app.listen(3000, () => {
console.log('Server running on http://localhost:3000')
})8. Client Usage
Authentication Flow
typescript
// client/auth-client.ts
import { BaseClient } from '@adi-family/http'
import {
registerConfig,
loginConfig,
getMeConfig
} from '../contracts/auth'
import {
getMyProjectsConfig,
createProjectConfig,
deleteProjectConfig
} from '../contracts/projects'
class AuthenticatedClient {
private client: BaseClient
private token: string | null = null
constructor(baseUrl: string) {
this.client = new BaseClient({ baseUrl })
}
// Register new user
async register(email: string, password: string, name: string) {
const result = await this.client.run(registerConfig, {
body: { email, password, name }
})
this.token = result.token
return result
}
// Login
async login(email: string, password: string) {
const result = await this.client.run(loginConfig, {
body: { email, password }
})
this.token = result.token
return result
}
// Get current user
async getMe() {
if (!this.token) throw new Error('Not authenticated')
return this.client.run(getMeConfig, {
headers: { Authorization: `Bearer ${this.token}` }
})
}
// Get user's projects
async getProjects(page = 1, limit = 10) {
if (!this.token) throw new Error('Not authenticated')
return this.client.run(getMyProjectsConfig, {
query: { page, limit },
headers: { Authorization: `Bearer ${this.token}` }
})
}
// Create project
async createProject(name: string, description: string) {
if (!this.token) throw new Error('Not authenticated')
return this.client.run(createProjectConfig, {
body: { name, description },
headers: { Authorization: `Bearer ${this.token}` }
})
}
// Delete project
async deleteProject(id: string) {
if (!this.token) throw new Error('Not authenticated')
return this.client.run(deleteProjectConfig, {
params: { id },
headers: { Authorization: `Bearer ${this.token}` }
})
}
// Logout
logout() {
this.token = null
}
}
// Usage
async function main() {
const client = new AuthenticatedClient('http://localhost:3000')
// Register
console.log('Registering user...')
const user = await client.register(
'john@example.com',
'password123',
'John Doe'
)
console.log('Registered:', user)
// Get current user
const me = await client.getMe()
console.log('Current user:', me)
// Create project
const project = await client.createProject(
'My Project',
'A test project'
)
console.log('Created project:', project)
// Get projects
const projects = await client.getProjects()
console.log('Projects:', projects)
// Delete project
await client.deleteProject(project.id)
console.log('Project deleted')
// Logout
client.logout()
}
main()Token Persistence
typescript
// client/persistent-auth-client.ts
class PersistentAuthClient extends AuthenticatedClient {
constructor(baseUrl: string) {
super(baseUrl)
// Load token from storage on initialization
this.token = localStorage.getItem('auth_token')
}
async login(email: string, password: string) {
const result = await super.login(email, password)
// Save token to storage
localStorage.setItem('auth_token', result.token)
return result
}
logout() {
super.logout()
localStorage.removeItem('auth_token')
}
}Best Practices
1. Use Environment Variables for Secrets
typescript
const JWT_SECRET = process.env.JWT_SECRET
if (!JWT_SECRET) {
throw new Error('JWT_SECRET must be set')
}2. Implement Token Refresh
typescript
async refreshToken() {
if (!this.token) throw new Error('Not authenticated')
const result = await this.client.run(refreshTokenConfig, {
headers: { Authorization: `Bearer ${this.token}` }
})
this.token = result.token
localStorage.setItem('auth_token', result.token)
}3. Handle 401 Errors Globally
typescript
async function apiCall<T>(fn: () => Promise<T>): Promise<T> {
try {
return await fn()
} catch (error) {
if (error instanceof Error && error.message.includes('401')) {
// Redirect to login
window.location.href = '/login'
}
throw error
}
}4. Use HTTPS in Production
Never send tokens over HTTP in production.
Next Steps
- Basic Examples - CRUD operations
- Advanced Examples - Complex patterns
- File Upload - Handle file uploads