Middleware
Middleware functions allow you to intercept and process requests before they reach your handlers.
Overview
Middleware runs in order:
- Handler Middlewares — Run before all REST handlers
- Your Handler — Your custom endpoint logic
- Error Middlewares — Catch and handle errors
Handler Middlewares
Global middleware that runs before all handlers in the handlers array.
Basic Example
import { defineConfig } from 'vite'
// import mockApi from '@ndriadev/vite-plugin-universal-api' //Default export
import { universalApi } from '@ndriadev/vite-plugin-universal-api' // Named export
export default defineConfig({
plugins: [
universalApi({
endpointPrefix: '/api',
handlerMiddlewares: [
// Logger middleware
async (req, res, next) => {
console.log(`${req.method} ${req.url}`)
next()
},
// Timing middleware
async (req, res, next) => {
const start = Date.now()
next()
console.log(`Request took ${Date.now() - start}ms`)
}
],
handlers: [
{
pattern: '/users',
method: 'GET',
handle: async (req, res) => {
res.writeHead(200)
res.end('Users')
}
}
]
})
]
})Middleware Function Signature
type UniversalApiMiddleware<TBody = unknown> = (
req: UniversalApiRequest<TBody>,
res: ServerResponse,
next: Connect.NextFunction
) => void | Promise<void>The generic TBody parameter lets you type the request body when you know its shape:
import type { UniversalApiMiddleware } from '@ndriadev/vite-plugin-universal-api'
interface AuthenticatedBody {
currentUser: { id: string; role: string }
}
// Typed middleware — body shape is known
const authMiddleware: UniversalApiMiddleware<AuthenticatedBody> = async (req, res, next) => {
const token = req.headers.authorization?.replace('Bearer ', '')
if (!token) {
res.writeHead(401, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: 'Unauthorized' }))
return
}
req.body.currentUser = await verifyToken(token)
next()
}Parameters:
req— Request object with parsedbody,params,query,filesres— Node.jsServerResponsenext— Call to proceed to next middleware/handler
Common Middleware Patterns
Authentication
handlerMiddlewares: [
async (req, res, next) => {
const token = req.headers.authorization?.replace('Bearer ', '')
if (!token) {
res.writeHead(401, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: 'No token provided' }))
return // Don't call next()
}
try {
const user = await verifyToken(token)
// Cast to any to attach a user to the body object
;(req.body as any).user = user
next()
} catch {
res.writeHead(401, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: 'Invalid token' }))
}
}
]Typed body after auth
If your handlers always expect an authenticated body, declare a shared body type and use it in both middleware and handler:
import type { UniversalApiMiddleware, UniversalApiSimpleHandler } from '@ndriadev/vite-plugin-universal-api'
interface AuthBody { user: { id: string; role: string } }
const authMiddleware: UniversalApiMiddleware<AuthBody> = async (req, res, next) => {
const user = await verifyToken(req.headers.authorization!)
req.body.user = user
next()
}
const usersHandler: UniversalApiSimpleHandler<AuthBody> = async (req, res) => {
const { user } = req.body // fully typed ✅
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ requestedBy: user.id }))
}Request Validation
handlerMiddlewares: [
async (req, res, next) => {
if (['POST', 'PUT', 'PATCH'].includes(req.method!)) {
if (!req.body || typeof req.body !== 'object') {
res.writeHead(400, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: 'Invalid request body' }))
return
}
}
next()
}
]CORS Headers
handlerMiddlewares: [
async (req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
if (req.method === 'OPTIONS') {
res.writeHead(204)
res.end()
return
}
next()
}
]Rate Limiting
const rateLimits = new Map<string, number[]>()
handlerMiddlewares: [
async (req, res, next) => {
const ip = String(req.headers['x-forwarded-for'] ?? req.socket.remoteAddress ?? 'unknown')
const now = Date.now()
const windowMs = 60_000 // 1 minute
const maxRequests = 100
const requests = (rateLimits.get(ip) ?? []).filter(t => now - t < windowMs)
if (requests.length >= maxRequests) {
res.writeHead(429, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: 'Too many requests' }))
return
}
requests.push(now)
rateLimits.set(ip, requests)
next()
}
]Request ID
import { randomUUID } from 'crypto'
handlerMiddlewares: [
async (req, res, next) => {
const requestId = randomUUID()
;(req.body as any).requestId = requestId
res.setHeader('X-Request-ID', requestId)
next()
}
]Error Middlewares
Handle errors that occur during request processing.
Basic Error Handler
errorMiddlewares: [
(err, req, res, next) => {
console.error('Error:', err)
res.writeHead(500, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: 'Internal server error' }))
}
]Error Handler Signature
type UniversalApiErrorMiddleware = (
err: any,
req: UniversalApiRequest | IncomingMessage,
res: ServerResponse,
next: Connect.NextFunction
) => void | Promise<void>Note that req may be either a fully parsed UniversalApiRequest or a raw IncomingMessage, depending on when the error occurred in the pipeline. Guard accordingly:
errorMiddlewares: [
(err, req, res, next) => {
const body = 'body' in req ? req.body : null
// ...
}
]Multiple Error Handlers
errorMiddlewares: [
// Handle specific error types
(err, req, res, next) => {
if (err.name === 'ValidationError') {
res.writeHead(400, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: 'Validation failed', details: err.details }))
return
}
if (err.name === 'NotFoundError') {
res.writeHead(404, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: 'Resource not found' }))
return
}
next(err) // Pass to next error handler
},
// Generic catch-all
(err, req, res, next) => {
console.error('Unhandled error:', err)
res.writeHead(500, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: 'Internal server error' }))
}
]Middleware Execution Order
Request
↓
handlerMiddlewares[0]
↓
handlerMiddlewares[1]
↓
handlerMiddlewares[n]
↓
Handler (if match found)
↓
Response
(If error occurs anywhere above)
↓
errorMiddlewares[0]
↓
errorMiddlewares[1]
↓
errorMiddlewares[n]
↓
ResponseComplete Example
import { defineConfig } from 'vite'
import { universalApi } from '@ndriadev/vite-plugin-universal-api'
import type { UniversalApiSimpleHandler } from '@ndriadev/vite-plugin-universal-api'
interface AppBody {
currentUser?: { id: string; role: string }
requestId?: string
}
const users = new Map<string, { id: string; role: string }>()
const getUsers: UniversalApiSimpleHandler<AppBody> = async (req, res) => {
const { currentUser } = req.body // typed ✅
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ requestedBy: currentUser?.id, users: [...users.values()] }))
}
export default defineConfig({
plugins: [
universalApi({
endpointPrefix: '/api',
handlerMiddlewares: [
// 1. Logger
async (req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`)
next()
},
// 2. CORS
async (req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
if (req.method === 'OPTIONS') {
res.writeHead(204)
res.end()
return
}
next()
},
// 3. Request ID
async (req, res, next) => {
;(req.body as AppBody).requestId = crypto.randomUUID()
next()
},
// 4. Authentication
async (req, res, next) => {
const publicPaths = ['/api/login', '/api/register']
if (publicPaths.some(p => req.url!.startsWith(p))) {
next()
return
}
const token = req.headers.authorization?.replace('Bearer ', '')
const user = token ? users.get(token) : undefined
if (!user) {
res.writeHead(401, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: 'Unauthorized' }))
return
}
;(req.body as AppBody).currentUser = user
next()
}
],
errorMiddlewares: [
(err, req, res, next) => {
console.error('Error:', err)
if (err.name === 'ValidationError') {
res.writeHead(400, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: err.message }))
return
}
next(err)
},
(err, req, res, next) => {
res.writeHead(500, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: 'Internal server error' }))
}
],
handlers: [
{
pattern: '/users',
method: 'GET',
handle: getUsers
}
]
})
]
})Important Notes
Scope
⚠️ Handler middlewares only run for handlers in the handlers array
They do NOT run for:
- Pure file-system requests (when no REST handler matches)
- WebSocket connections
Calling next()
Always call next() to continue the chain:
// ✅ Good
async (req, res, next) => {
doSomething()
next()
}
// ✅ Good — early return on error
async (req, res, next) => {
if (error) {
res.writeHead(400)
res.end('Error')
return // Don't call next
}
next()
}
// ❌ Bad — request hangs
async (req, res, next) => {
doSomething()
// Forgot next()!
}Typed Body
The body field on UniversalApiRequest is typed as TBody = unknown by default. Inside middleware, use a cast when attaching data that subsequent handlers will consume:
;(req.body as MyBodyShape).myField = valueOr declare the middleware with an explicit generic:
const myMiddleware: UniversalApiMiddleware<MyBodyShape> = async (req, res, next) => {
req.body.myField = value // ✅ typed
next()
}Best Practices
1. Order Matters
Place middleware in logical order:
- Logging (first)
- CORS
- Request ID
- Authentication
- Validation / business logic
2. Early Returns
Return early for errors to avoid calling next():
if (error) {
res.writeHead(400)
res.end('Error')
return // Don't call next()
}
next()3. Don't Modify res After Calling next()
// ❌ Bad
next()
res.setHeader('X-Custom', 'value') // Too late!
// ✅ Good
res.setHeader('X-Custom', 'value')
next()4. Use Try-Catch for Async
async (req, res, next) => {
try {
await riskyOperation()
next()
} catch (err) {
res.writeHead(500)
res.end('Error')
}
}Next Steps
- REST Handlers — Handler configuration
- Examples — Authentication — Full auth example
- API Reference — Complete configuration
