File: middleware.md | Updated: 11/15/2025
Hide navigation
Search
Ctrl K
Home Guides EAS Reference Learn
Archive Expo Snack Discord and Forums Newsletter
Copy page
Learn how to create middleware that runs for every request to the server in Expo Router.
Copy page
Server middleware is an experimental feature available in SDK 54 and later, and requires a deployed server for production use.
Server middleware in Expo Router allows you to run code before requests reach your routes, enabling powerful server-side functionality like authentication and logging for every request. Unlike API routes
that handle specific endpoints, middleware runs for every request in your app, so it should run as quickly as possible to avoid slowing down your app's performance. Client-side navigation such as on native, or in a web app when using <Link />
, will not move through the server middleware.
1
First, configure your app to use server output by adding the server configuration to your app config :
app.json
Copy
{ "expo": { %%placeholder-start%%... %%placeholder-end%% "web": { "output": "server" }, "plugins": [ [ "expo-router", { "unstable_useServerMiddleware": true } ] ] } }
2
Create a +middleware.ts file in your app directory, to define your server middleware function:
app/+middleware.ts
Copy
export default function middleware(request) { console.log(`Middleware executed for: ${request.url}`); // Your middleware logic goes here }
The middleware function must be the default export of the file. It receives an immutable request
and can return either a Response
, or nothing to let the request pass through unmodified. The request is immutable to prevent side effects; you can read headers and properties, but you cannot modify headers or consume the request body.
3
Run your development server to test the middleware:
Terminal
Copy
- npx expo start
Your middleware will now run for all requests to your app.
4
Visit your app in a browser or make requests to test that your middleware is working. Check your console for the log messages from the middleware function.
5
By default, middleware runs on all server requests. You can add a matcher to control when your middleware executes with unstable_settings:
app/+middleware.ts
Copy
export const unstable_settings = { matcher: { // Only run on GET requests methods: ['GET'], // Only run on API routes and specific paths patterns: ['/api', '/admin/[...path]'], }, }; export default function middleware(request) { console.log(`Middleware executed for: ${request.url}`); }
The matcher configuration allows you to:
Middleware functions are executed before any route handlers, allowing you to perform actions like logging, authentication, or modifying responses. It runs exclusively on the server and only for actual HTTP requests.
When a request comes to your app, Expo Router processes it in this order:
Response, that response is sent immediatelyMatchers support different pattern types to control when middleware runs:
export const unstable_settings = { matcher: { patterns: [ '/api', // Exact path '/posts/[postId]', // Named parameter '/blog/[...slug]', // Catch-all parameter /^\/api\/v\d+\/users$/, // Regular expression ], }, };
/api matches /api but not /api/users[postId] capture any single segment. /posts/[postId] matches /posts/123 or /posts/my-post[...slug] capture one or more segments. /blog/[...slug] matches /blog/2024 or /blog/2024/12/post/^\/api\/v\d+\/users$/ matches /api/v1/users but not /api/usersMiddleware runs if any pattern matches the request URL. When both methods and patterns are specified, both conditions must be met for middleware to run.
Expo Router supports a single middleware file named +middleware.ts that runs for all server requests. When using matchers, middleware executes only for requests that match the specified patterns and methods, before any route matching or rendering occurs.
Middleware executes only for actual HTTP requests to your server. This means it is executed for:
Middleware does not run for:
Native app screen transitions
Prefetched routes
Static asset requests like images and fonts
Middleware is often used to perform authorization checks before a route has loaded. You can check headers, cookies, or query parameters to determine if a user has access to certain routes:
app/+middleware.ts
Copy
import { jwtVerify } from 'jose'; export default function middleware(request) { const token = request.headers.get('authorization'); const decoded = jwtVerify(token, process.env.SECRET_KEY); if (!decoded.payload) { return new Response('Forbidden', { status: 403 }); } }
Logging
You can use middleware to log requests for debugging or analytics purposes. This can help you track user activity or diagnose issues in your app:
app/+middleware.ts
Copy
export default function middleware(request) { console.log(`${request.method} ${request.url}`); }
Dynamic redirects
Middleware can also be used to perform dynamic redirects. This allows you to control user navigation based on specific conditions:
app/+middleware.ts
Copy
export default function middleware(request) { if (request.headers.has('specific-header')) { return Response.redirect('https://expo.dev'); } }
API-only middleware
Use matchers to run middleware only for API routes, keeping other routes unaffected:
app/+middleware.ts
Copy
export const unstable_settings = { matcher: { patterns: ['/api'], }, }; export default function middleware(request) { // Log all API requests for debugging console.log(`API request: ${request.method} ${request.url}`); // Add CORS headers for API routes const response = new Response(); response.headers.set('Access-Control-Allow-Origin', '*'); return response; }
Method-specific authentication
Protect write operations (POST, PUT, DELETE) while allowing public read access:
app/+middleware.ts
Copy
export const unstable_settings = { matcher: { methods: ['POST', 'PUT', 'DELETE'], patterns: ['/api', '/admin/[...path]'], }, }; export default function middleware(request) { const token = request.headers.get('authorization'); if (!token || !isValidToken(token)) { return new Response('Unauthorized', { status: 401 }); } } function isValidToken(token: string): boolean { // Your token validation logic return token.startsWith('Bearer '); }
Show More
Monitor specific endpoints without logging every request:
app/+middleware.ts
Copy
export const unstable_settings = { matcher: { patterns: ['/api/users/[userId]', '/admin', /^\/webhook/], }, }; export default function middleware(request) { const userAgent = request.headers.get('user-agent'); const timestamp = new Date().toISOString(); console.log(`[${timestamp}] ${request.method} ${request.url} - ${userAgent}`); }
app/+middleware.ts
Copy
import { MiddlewareFunction } from 'expo-router/server'; const middleware: MiddlewareFunction = request => { if (request.headers.has('specific-header')) { return Response.redirect('https://expo.dev'); } }; export default middleware;
<Link />
or native app screen transitions.To prevent unintended side effects and ensure the request body remains available for route handlers, the Request
passed to middleware is immutable. This means you can:
url, method, headers, and so onrequest.headers.get()request.headers.has()But you won't be able to:
set(), append(), delete()text(), json(), formData(), and so onbody property directly