Middlewares let you intercept requests before they reach your routes. They’re useful for authentication, logging, redirects, adding headers, and more.
Place a _middleware.ts file anywhere inside your app directory to create a middleware.
_layout.tsx
Wraps all files in this directory and below
_middleware.ts
Middleware for all "/" routes
index.tsx
Matches "/"
blog
_middleware.ts
Middleware for all "/blog" routes
index.tsx
Matches "/blog"
api
_middleware.ts
Middleware for all "/api" routes
users+api.ts
API route at "/api/users"
Middlewares are hierarchical - they run from the root down to the most specific matching route. In the example above, a request to /blog would run:
app/_middleware.ts firstapp/blog/_middleware.ts secondapp/_middleware.ts
import { createMiddleware } from 'one'
export default createMiddleware(async ({ request, next }) => {
console.log(`[${new Date().toISOString()}] ${request.method} ${request.url}`)
const response = await next()
return response
})
The createMiddleware function accepts a middleware handler and provides type safety:
import { createMiddleware, type Middleware } from 'one'
type MiddlewareProps = {
request: Request // Standard Web API Request
next: () => Promise<Response | null> // Call the next middleware/route
context: Record<string, any> // Shared mutable object for data passing
}
export default createMiddleware(async ({ request, next, context }: MiddlewareProps) => {
// Your middleware logic
})
| Prop | Type | Description |
|---|---|---|
request | Request | The incoming request object |
next | () => Promise<Response | null> | Calls the next middleware in the chain, then the route |
context | object | Mutable object shared across all middlewares in the chain |
| Return | Effect |
|---|---|
Response | Stops the chain and returns this response immediately |
await next() | Continues to the next middleware/route and returns its response |
null or undefined | Continues the chain (same as calling next()) |
Protect routes by checking authentication before the request reaches your route:
app/dashboard/_middleware.ts
import { createMiddleware } from 'one'
export default createMiddleware(async ({ request, next, context }) => {
const token = request.headers.get('authorization')?.replace('Bearer ', '')
if (!token) {
return new Response('Unauthorized', {
status: 401,
headers: { 'WWW-Authenticate': 'Bearer' }
})
}
// Verify token and add user to context
try {
const user = await verifyToken(token)
context.user = user
} catch {
return new Response('Invalid token', { status: 401 })
}
return await next()
})
Redirect users based on conditions:
app/_middleware.ts
import { createMiddleware } from 'one'
export default createMiddleware(async ({ request, next }) => {
const url = new URL(request.url)
// Redirect old paths to new paths
if (url.pathname === '/old-page') {
return Response.redirect(new URL('/new-page', url.origin), 301)
}
// Redirect to HTTPS in production
if (url.protocol === 'http:' && process.env.NODE_ENV === 'production') {
url.protocol = 'https:'
return Response.redirect(url, 301)
}
return await next()
})
Modify response headers for CORS, caching, security, etc:
app/api/_middleware.ts
import { createMiddleware, setResponseHeaders } from 'one'
export default createMiddleware(async ({ request, next }) => {
// Add CORS headers using setResponseHeaders helper
setResponseHeaders((headers) => {
headers.set('Access-Control-Allow-Origin', '*')
headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')
})
// Handle preflight requests
if (request.method === 'OPTIONS') {
return new Response(null, { status: 204 })
}
return await next()
})
Or modify the response directly:
app/_middleware.ts
import { createMiddleware } from 'one'
export default createMiddleware(async ({ request, next }) => {
const response = await next()
// Add security headers to all responses
response?.headers.set('X-Content-Type-Options', 'nosniff')
response?.headers.set('X-Frame-Options', 'DENY')
return response
})
Log requests for debugging or analytics:
app/_middleware.ts
import { createMiddleware } from 'one'
export default createMiddleware(async ({ request, next }) => {
const start = Date.now()
const url = new URL(request.url)
const response = await next()
const duration = Date.now() - start
console.log(`${request.method} ${url.pathname} - ${response?.status} (${duration}ms)`)
return response
})
Pass data between middlewares and to your route handlers:
app/_middleware.ts
import { createMiddleware } from 'one'
export default createMiddleware(async ({ request, next, context }) => {
// Parse and validate request data once
context.requestId = crypto.randomUUID()
context.timestamp = Date.now()
// Add user info if authenticated
const token = request.headers.get('authorization')
if (token) {
context.user = await getUserFromToken(token)
}
return await next()
})
Access the context in nested middlewares:
app/api/_middleware.ts
import { createMiddleware } from 'one'
export default createMiddleware(async ({ request, next, context }) => {
// Context from parent middleware is available
console.log(`Request ${context.requestId} from user ${context.user?.id}`)
return await next()
})
Return a response immediately without calling next() to intercept the request:
app/_middleware.ts
import { createMiddleware } from 'one'
export default createMiddleware(async ({ request, next }) => {
const url = new URL(request.url)
// Return cached response for specific paths
if (url.pathname === '/api/health') {
return Response.json({ status: 'ok', timestamp: Date.now() })
}
// Block certain paths
if (url.pathname.startsWith('/admin') && !isAdmin(request)) {
return new Response('Forbidden', { status: 403 })
}
return await next()
})
Modify responses after the route has processed:
app/_middleware.ts
import { createMiddleware } from 'one'
export default createMiddleware(async ({ request, next }) => {
const response = await next()
// Handle 404s with a custom response
if (!response || response.status === 404) {
return Response.json(
{ error: 'Not found', path: new URL(request.url).pathname },
{ status: 404 }
)
}
return response
})
Middlewares execute in order from root to leaf:
Terminal
Request to /blog/post/123
↓
1. app/_middleware.ts (runs first)
↓
2. app/blog/_middleware.ts (runs second)
↓
3. app/blog/post/_middleware.ts (runs third)
↓
4. Route handler executes
↓
Responses bubble back up through each middleware
Each middleware can:
next()next() to continue the chainnext()| Platform | Support |
|---|---|
| Node.js server | Full support |
| Vercel | Full support |
| Cloudflare Workers | Full support |
| Static export | Not applicable |
| React Native | Not supported (server-only) |
Middlewares are server-only and run on every request. They do not run during static site generation or on native platforms.
Edit this page on GitHub.