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

_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:

  1. app/_middleware.ts first
  2. app/blog/_middleware.ts second
  3. Then the route handler

Basic Usage

app/_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
})

API Reference

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
})

Props

PropTypeDescription
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

ReturnEffect
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

Authentication

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()
})

Redirects

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()
})

Adding Headers

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
})

Logging

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
})

Sharing Data with Context

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()
})

Early Response (Interception)

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()
})

Post-Processing Responses

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
})

Execution Order

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:

  • Intercept: Return a response early without calling next()
  • Pass through: Call next() to continue the chain
  • Post-process: Modify the response after calling next()

Platform Support

PlatformSupport
Node.js serverFull support
VercelFull support
Cloudflare WorkersFull support
Static exportNot applicable
React NativeNot 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.