Server middleware
Edit this page
Learn how to create middleware that runs for every request to the server in Expo Router.
Server middleware is an experimental feature, 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.
Setup
1
Enable server middleware in your app configuration
First, configure your app to use server output by adding the server configuration to your app config:
{ "expo": { %%placeholder-start%%... %%placeholder-end%% "web": { "output": "server" }, "plugins": [ [ "expo-router", { "unstable_useServerMiddleware": true } ] ] } }
2
Create your middleware file
Create a +middleware.ts file in your app directory, to define your server middleware function:
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
4
How it works
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.
Request/response flow
When a request comes to your app, Expo Router processes it in this order:
- The middleware function runs first with an immutable request.
- If middleware returns a
Response
, that response is sent immediately - If middleware returns nothing, the request continues to the matching route
- The route handler processes the request and returns its response
Middleware execution order
Expo Router supports a single middleware file named +middleware.ts that runs for all server requests. Middleware executes before any route matching or rendering occurs, letting you control the request lifecycle.
When middleware runs
Middleware executes only for actual HTTP requests to your server. This means it is executed for:
- Initial page loads, like when a user first visits your site
- Full page refreshes
- Direct URL navigation
- API route calls from any client (native/web apps, external services)
- Server-side rendering requests
Middleware does not run for:
- Client-side navigation using
<Link />
orrouter
- Native app screen transitions
- Prefetched routes
- Static asset requests like images and fonts
Examples
Authentication
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:
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:
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:
export default function middleware(request) { if (request.headers.has('specific-header')) { return Response.redirect('https://expo.dev'); } }
Additional notes
Best practices
- Keep middleware lightweight because it runs synchronously on every server request and directly impacts response times.
- For native apps, use API routes for secure data fetching. When native apps call API routes, those requests will pass through middleware first.
Typed middleware
import { MiddlewareFunction } from 'expo-router'; const middleware: MiddlewareFunction = request => { if (request.headers.has('specific-header')) { return Response.redirect('https://expo.dev'); } }; export default middleware;
Limitations
- Middleware runs exclusively on the server and only for HTTP requests. It does not execute during client-side navigation, for example, with
<Link />
or native app screen transitions. - The request object passed to middleware is immutable to prevent side effects. You cannot modify headers or consume the request body, ensuring it remains available for route handlers.
- You can only have one root-level +middleware.ts in your app.
- The same limitations that apply to API routes also apply to middleware.
Request immutability
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:
- Read all request properties like
url
,method
,headers
, and so on - Read header values using
request.headers.get()
- Check for header existence with
request.headers.has()
- Access URL parameters and query strings
But you won't be able to:
- Modify headers with
set()
,append()
,delete()
- Consume the request body with
text()
,json()
,formData()
, and so on - Access the
body
property directly