Learn how to create server endpoints with Expo Router.
This feature is still experimental. Available from Expo SDK 50 and Expo Router v3.
Expo Router enables you to write server code for all platforms, right in your app directory.
{
"web": {
"bundler": "metro",
"output": "server"
}
}
Server features require a custom Node.js server. Most hosting providers support Node.js, including Netlify, Cloudflare, and Vercel.
API Routes are functions that are executed when a route is matched. They can be used to handle sensitive data, such as API keys securely, or implement custom server logic. API Routes should be executed in a WinterCG-compliant environment.
API Routes are defined by creating files in the app directory with the +api.js
extension. For example, the following route handler is executed when the route /hello
is matched.
app
index.js
hello+api.ts
API Route
1
An API route is created in the app directory. For example, add the following route handler. It is executed when the route /hello
is matched.
import { ExpoRequest, ExpoResponse } from 'expo-router/server';
export function GET(request: ExpoRequest) {
return ExpoResponse.json({ hello: 'world' });
}
You can export any of the following functions GET
, POST
, PUT
, PATCH
, DELETE
, HEAD
, and OPTIONS
from a server route. The function executes when the corresponding HTTP method is matched. Unsupported methods will automatically return 405: Method not allowed
.
2
Start the development server with Expo CLI:
-
npx expo
3
You can make a network request to the route to access the data. Run the following command to test the route:
-
curl http://localhost:8081/hello
You can also make a request from the client code:
import { Button } from 'react-native';
async function fetchHello() {
const response = await fetch('/hello');
const data = await response.json();
alert('Hello ' + data.hello);
}
export default function App() {
return <Button onPress={() => fetchHello()} title="Fetch hello" />;
}
This won't work by default on native as /hello
does not provide an origin URL. You can configure the origin URL in the app config file. It can be a mock URL in development. For example:
{
"plugins": [
[
"expo-router",
{
"origin": "https://evanbacon.dev/"
}
]
]
}
4
Deploy the website and server to a hosting provider to access the routes in production on both native and web.
Requests use the global ExpoRequest
object. It is a subclass of the standard Request
object and provides additional functionality such as the expoUrl
object — a URL
that has access to query parameters according to the Expo Router file convention.
import { ExpoRequest, ExpoResponse } from 'expo-router/server';
export async function GET(request: ExpoRequest, { post }: Record<string, string>) {
// const postId = request.expoUrl.searchParams.get('post')
// fetch data for 'post'
return ExpoResponse.json({ ... });
}
Use the request.json()
function to access the request body. It automatically parses the body and returns the result.
import { ExpoRequest, ExpoResponse } from 'expo-router/server';
export async function POST(request: ExpoRequest) {
const body = await request.json();
return ExpoResponse.json({ ... });
}
Responses use the global ExpoResponse
object. It is a subclass of the standard Response
object and provides additional functionality such as the ExpoResponse.json
— an initializer which automatically stringifies the response body and returns status 200
.
import { ExpoRequest, ExpoResponse } from 'expo-router/server';
export function GET() {
return ExpoResponse.json({ hello: 'universe' });
}
You can respond to server errors by using the ExpoResponse
object.
import { ExpoRequest, ExpoResponse } from 'expo-router/server';
export async function GET(request: ExpoRequest, { post }: Record<string, string>) {
if (!post) {
return new ExpoResponse('No post found', {
status: 404,
headers: {
'Content-Type': 'text/plain',
},
});
}
// fetch data for `post`
return ExpoResponse.json({ ... });
}
Making requests with an undefined method will automatically return 405: Method not allowed
. If an error is thrown during the request, it will automatically return 500: Internal server error
.
API Routes are bundled with Expo CLI and Metro bundler. They have access to all of the language features as your client code:
EXPO_PUBLIC_
.Route handlers are executed in a sandboxed environment that is isolated from the client code. It means you can safely store sensitive data in the route handlers without exposing it to the client.
expo/metro-config
and requires it to be used in the metro.config.js.This is experimental and subject to breaking changes. We have no continuous tests against this configuration.
Every cloud hosting provider needs a custom adapter to support the Expo server runtime. The following third-party providers have unofficial or experimental support from the Expo team.
Before deploying to these providers, it may be good to be familiar with the basics of npx expo export
command:
@expo/server
package is included with expo
and delegates requests to the server routes.@expo/server
does not inflate environment variables from .env files. They are expected to load either by the hosting provider or the user.1
Install the required dependencies:
-
npm i -D express compression morgan
2
Export the website for production:
-
npx expo export -p web
3
Write a server entry file that serves the static files and delegates requests to the server routes:
#!/usr/bin/env node
const path = require('path');
const { createRequestHandler } = require('@expo/server/adapter/express');
const express = require('express');
const compression = require('compression');
const morgan = require('morgan');
const CLIENT_BUILD_DIR = path.join(process.cwd(), 'dist/client');
const SERVER_BUILD_DIR = path.join(process.cwd(), 'dist/server');
const app = express();
app.use(compression());
// http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header
app.disable('x-powered-by');
process.env.NODE_ENV = 'production';
app.use(
express.static(CLIENT_BUILD_DIR, {
maxAge: '1h',
extensions: ['html'],
})
);
app.use(morgan('tiny'));
app.all(
'*',
createRequestHandler({
build: SERVER_BUILD_DIR,
})
);
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Express server listening on port ${port}`);
});
4
Start the server with node
command:
-
node server.js
This is experimental and subject to breaking changes. We have no continuous tests against this configuration.
1
Create a server entry file. All requests will be delegated through this middleware. The exact file location is important.
const { createRequestHandler } = require('@expo/server/adapter/netlify');
const handler = createRequestHandler({
build: require('path').join(__dirname, '../../dist/server'),
mode: process.env.NODE_ENV,
});
module.exports = { handler };
2
Create a Netlify configuration file at the root of your project to redirect all requests to the server function.
[build]
command = "expo export -p web"
functions = "netlify/functions"
publish = "dist/client"
[[redirects]]
from = "/*"
to = "/.netlify/functions/server"
status = 404
[functions]
# Include everything to ensure dynamic routes can be used.
included_files = ["dist/server/**/*"]
[[headers]]
for = "/dist/server/_expo/functions/*"
[headers.values]
# Set to 60 seconds as an example.
"Cache-Control" = "public, max-age=60, s-maxage=60"
3
After you have created the configuration files, you can build the website and functions with Expo CLI:
-
npx expo export -p web
4
Deploy to Netlify with the Netlify CLI.
# Install the Netlify CLI globally if needed.
-
npm install netlify-cli -g
# Deploy the website.
-
netlify deploy
You can now visit your website at the URL provided by Netlify CLI. Running netlify deploy --prod
will publish to the production URL.
5
If you're using any environment variables or .env files, add them to Netlify. You can do this by going to the Site settings and adding them to the Build & deploy section.
This is experimental and subject to breaking changes. We have no continuous tests against this configuration.
1
Create a server entry file. All requests will be delegated through this middleware. The exact file location is important.
const { createRequestHandler } = require('@expo/server/adapter/vercel');
module.exports = createRequestHandler({
build: require('path').join(__dirname, '../dist/server'),
mode: process.env.NODE_ENV,
});
2
Create a Vercel configuration file (vercel.json) at the root of your project to redirect all requests to the server function.
{
"version": 2,
"outputDirectory": "dist",
"builds": [
{
"src": "package.json",
"use": "@vercel/static-build",
"config": {
"distDir": "dist/client"
}
},
{
"src": "api/index.js",
"use": "@vercel/node",
"config": {
"includeFiles": ["dist/server/**"]
}
}
],
"routes": [
{
"handle": "filesystem"
},
{
"src": "/(.*)",
"dest": "/api/index.js"
}
]
}
The legacy version of the vercel.json needs a @vercel/static-build
runtime to serve your assets from the dist/client output directory.
{
"buildCommand": "expo export -p web",
"outputDirectory": "dist/client",
"functions": {
"api/index.js": {
"runtime": "@vercel/node@3.0.11",
"includeFiles": "dist/server/**"
}
},
"rewrites": [
{
"source": "/(.*)",
"destination": "/api/index.js"
}
]
}
The newer version of the vercel.json does not use routes
and builds
configuration options anymore, and serves your public assets from the dist/client output directory automatically.
3
After you have created the configuration files, add a vercel-build
script to your package.json file and set it to expo export -p web
.
4
Deploy to Vercel with the Vercel CLI.
# Install the Vercel CLI globally if needed.
-
npm install vercel -g
# Deploy the website.
-
vercel deploy
You can now visit your website at the URL provided by the Vercel CLI.
Several known features are not currently supported in the API Routes beta release.
API Routes currently work by bundling all code (minus the Node.js built-ins) into a single file. This means that you cannot use any external dependencies that are not bundled with the server. For example, a library such as sharp
, which includes multiple platform binaries, cannot be used. This will be addressed in a future version.
The current bundling implementation opts to be more unified than flexible. This means the limitation of native not supporting ESM is carried over to API Routes. All code will be transpiled down to Common JS (require
/module.exports
). However, we recommend you write API Routes using ESM regardless. This will be addressed in a future version.