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.
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 secondimport { 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()) |
Redirect users based on conditions:
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:
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()})one dev and one serve — the same OPTIONS branch answers preflight requests in both. One configures Vite’s dev server with server.cors.preflightContinue: true so that Vite’s built-in cors middleware doesn’t intercept preflight before your middleware sees it. If you override server.cors in your vite.config.ts, keep preflightContinue: true. See API Routes → Dev and prod parity for details.Or modify the response directly:
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:
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:
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:
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:
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:
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:
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 middlewareEach 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.
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.