Middlewares 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.
File Structure
Wraps all files in this directory and below
Middleware for all "/" routes
Middleware for all "/blog" routes
Middleware for all "/api" routes
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 first
app/blog/_middleware.ts second
Then the route handler
Basic Usage
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
} )
API Reference
The createMiddleware function accepts a middleware handler and provides type safety:
import { createMiddleware, type Middleware } from 'one'
type MiddlewareProps = {
request: Request
next : ( ) => Promise < Response | null >
context: Record< string , any >
}
export default createMiddleware ( async ( { request, next, context } : MiddlewareProps) => {
} )
Props
Prop Type Description requestRequestThe incoming request object next() => Promise<Response | null>Calls the next middleware in the chain, then the route contextobjectMutable object shared across all middlewares in the chain
Return Values
Return Effect ResponseStops the chain and returns this response immediately await next()Continues to the next middleware/route and returns its response null or undefinedContinues the chain (same as calling next() )
Common Patterns
Redirects
Redirect users based on conditions:
import { createMiddleware } from 'one'
export default createMiddleware ( async ( { request, next } ) => {
const url = new URL ( request. url)
if ( url. pathname === '/old-page' ) {
return Response. redirect ( new URL ( '/new-page' , url. origin) , 301 )
}
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:
import { createMiddleware, setResponseHeaders } from 'one'
export default createMiddleware ( async ( { request, next } ) => {
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' )
} )
if ( request. method === 'OPTIONS' ) {
return new Response ( null , { status: 204 } )
}
return await next ( )
} )
Or modify the response directly:
import { createMiddleware } from 'one'
export default createMiddleware ( async ( { request, next } ) => {
const response = await next ( )
response?. headers. set ( 'X-Content-Type-Options' , 'nosniff' )
response?. headers. set ( 'X-Frame-Options' , 'DENY' )
return response
} )
Logging
Log requests for debugging or analytics:
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
} )
Sharing Data with Context
Pass data between middlewares and to your route handlers:
import { createMiddleware } from 'one'
export default createMiddleware ( async ( { request, next, context } ) => {
context. requestId = crypto. randomUUID ( )
context. timestamp = Date. now ( )
const token = request. headers. get ( 'authorization' )
if ( token) {
context. user = await getUserFromToken ( token)
}
return await next ( )
} )
Access the context in nested middlewares:
import { createMiddleware } from 'one'
export default createMiddleware ( async ( { request, next, context } ) => {
console . log ( ` Request ${ context. requestId} from user ${ context. user?. id} ` )
return await next ( )
} )
Early Response (Interception)
Return a response immediately without calling next() to intercept the request:
import { createMiddleware } from 'one'
export default createMiddleware ( async ( { request, next } ) => {
const url = new URL ( request. url)
if ( url. pathname === '/api/health' ) {
return Response. json ( { status: 'ok' , timestamp: Date. now ( ) } )
}
if ( url. pathname. startsWith ( '/admin' ) && ! isAdmin ( request) ) {
return new Response ( 'Forbidden' , { status: 403 } )
}
return await next ( )
} )
Post-Processing Responses
Modify responses after the route has processed:
import { createMiddleware } from 'one'
export default createMiddleware ( async ( { request, next } ) => {
const response = await next ( )
if ( ! response || response. status === 404 ) {
return Response. json (
{ error: 'Not found' , path: new URL ( request. url) . pathname } ,
{ status: 404 }
)
}
return response
} )
Execution Order
Middlewares execute in order from root to leaf:
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:
Intercept : Return a response early without calling next()Pass through : Call next() to continue the chainPost-process : Modify the response after calling next()
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.
Middleware vs Loader Redirects
Middleware redirects and loader redirects serve different purposes:
Middleware Redirects Loader Redirects Best for URL rewrites, HTTPS enforcement, path-level access control Protecting page data, auth guards for SSR pages Runs on Every request before the route When loader data is fetched Client-side nav Not involved (middleware only runs on server requests) Server intercepts redirect and sends metadata to client, preventing flash of protected content Data protection Returns early before route processes Prevents sensitive loader data from reaching the client
For protecting SSR routes with auth, prefer loader redirects — they handle both direct page loads and client-side <Link> navigation, ensuring no unauthorized data leaks. Use middleware redirects for request-level concerns like URL rewrites, HTTPS enforcement, and API route protection.
See the Loader Redirects guide for a complete walkthrough.
Edit this page on GitHub.