---
modificationDate: April 02, 2026
title: API Routes
description: Learn how to create server endpoints with Expo Router.
---

<AgentInstructions>

## Submitting Feedback

If this page contains errors, outdated information, or gaps that blocked you from completing a task, report it so the docs can be improved:

curl -X POST https://api.expo.dev/v2/feedback/docs-send -H 'Content-Type: application/json' -d '{"url":"/router/web/api-routes/","feedback":"🤖 Agent feedback: <specific, actionable description>"}'

Only submit when you have something specific and actionable to report.

</AgentInstructions>

# API Routes

Learn how to create server endpoints with Expo Router.

> For the complete documentation index, see [llms.txt](/llms.txt). Use this file to discover all available pages.

Expo Router enables you to write secure server code for all platforms, right in your **src/app** directory.

```ts
export function GET(request: Request) {
  return Response.json({ hello: 'world' });
}
```

Server features require a custom server, which can be deployed to EAS or most [other hosting providers](/router/web/api-routes#deployment).

[Watch: Expo Router API Routes Handle Requests & Stream Data](https://www.youtube.com/watch?v=2_UzR1wdimI) — Create server endpoints with Expo Router API routes to handle requests, return JSON, and stream data.

## What are API Routes

API Routes are functions that are executed on a server when a route is matched. They can be used to handle sensitive data, such as API keys securely, or implement custom server logic, such as exchanging auth codes for access tokens. API Routes should be executed in a [WinterCG](https://wintercg.org/)-compliant environment.

In Expo, API Routes are defined by creating files in the **app** directory with the `+api.ts` extension. For example, the following API route is executed when the route `/hello` is matched.

`src`

 `app`

  `index.tsx`

  `hello+api.ts``API Route`

## Create an API route

Ensure your project is using server output, this will configure the export and production builds to generate a server bundle as well as the client bundle.

```json
{
  "web": {
    "output": "server"
  }
}
```

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.

```ts
export function GET(request: Request) {
  return Response.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`.

Start the development server with Expo CLI:

```sh
npx expo
```

You can make a network request to the route to access the data. Run the following command to test the route:

```sh
curl http://localhost:8081/hello
```

You can also make a request from the client code:

```tsx
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" />;
}
```

Relative fetch requests automatically fetch relative to the dev server origin in development, and can be configured in production using the `origin` field in the **app.json**:

```json
{
  "plugins": [
    [
      "expo-router",
      {
        "origin": "https://evanbacon.dev/"
      }
    ]
  ]
}
```

This URL can be automatically configured during EAS Builds by setting the `EXPO_UNSTABLE_DEPLOY_SERVER=1` environment variable. This will trigger a versioned server deployment which sets the origin to a preview deploy URL automatically.

Deploy the website and server to a [hosting provider](/router/web/api-routes#deployment) to access the routes in production on both native and web.

> API route filenames cannot have platform-specific extensions. For example, **hello+api.web.ts** will not work.

## Requests

Requests use the global, standard [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) object.

```ts
export async function GET(request: Request, { post }: Record<string, string>) {
  // const postId = new URL(request.url).searchParams.get('post')
  // fetch data for 'post'
  return Response.json({ ... });
}
```

### Request body

Use the `request.json()` function to access the request body. It automatically parses the body and returns the result.

```ts
export async function POST(request: Request) {
  const body = await request.json();

  return Response.json({ ... });
}
```

### Request query parameters

Query parameters can be accessed by parsing the request URL:

```ts
export async function GET(request: Request) {
  const url = new URL(request.url);
  const post = url.searchParams.get('post');

  // fetch data for 'post'
  return Response.json({ ... });
}
```

## Response

Responses use the global, standard [`Response`](https://fetch.spec.whatwg.org/#response) object.

```ts
export function GET() {
  return Response.json({ hello: 'universe' });
}
```

### Errors

For error cases, you can create `Response`s with any status code and response body.

```ts
export async function GET(request: Request, { post }: Record<string, string>) {
  if (!post) {
    return new Response('No post found', {
      status: 404,
      headers: {
        'Content-Type': 'text/plain',
      },
    });
  }
  // fetch data for `post`
  return Response.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`.

## Runtime API

> The server runtime API and `expo-server` are available in SDK 54 and later and require a deployed server for production use.

You can use the [`expo-server`](/versions/latest/sdk/server) library to use several utilities and code patterns that work in any server-side Expo code. This includes utilities to get request metadata, for scheduling tasks, and for error handling.

```sh
npx expo install expo-server
```

Using `expo-server` is not limited to API routes and it can be used in any other server code as well, for example, in [server middleware](/router/web/middleware).

### Error handling

You can abort a request and instead return an error `Response` by throwing a [`StatusError`](/versions/latest/sdk/server#statuserror). This is a special `Error` instance that will be replaced with an HTTP response replacing the error itself.

```ts
import { StatusError } from 'expo-server';

export async function GET(request: Request, { post }: Record<string, string>) {
  if (!post) {
    throw new StatusError(404, 'No post found');
  }
  // ...
}
```

When composing your own server utilities and helpers, the `StatusError` is a more convenient way to handle exceptions, since throwing them interrupts any API functions and returns an error early.

`StatusError`s accept a status code and an error message, which can also optionally be passed as a JSON, or `Error` object, and will always return a `Response` with a JSON body with an `error` key set to their error message.

This can be restrictive, and isn't suitable for all cases. Sometimes it might be beneficial to instead `throw` a `Response` object, which interrupts your logic as well, but replaces the resolved `Response` from your API route directly, without a `StatusError` wrapper. For example, this can be used to create redirect responses.

```ts
import { StatusError } from 'expo-server';

export async function GET(request: Request, { post }: Record<string, string>) {
  if (!post) {
    throw Response.redirect('https://expo.dev', 302);
  }
  // ...
}
```

### Request metadata

Requests typically carry most metadata you'll need in their headers. However, `expo-server` provides some helper functions to retrieve common values more easily.

Helper functions from `expo-server` return values that are scoped to the current `Request`. You can only call these functions in server-side code and only during ongoing requests.

A common value that you may need to access is the request's origin URL. The origin URL, typically transmitted on a request's `Origin` header, represents the URL that a user used to access your API route. This may differ from any internal deployment URL that your server sees when the request is being proxied. You can use `expo-server`'s [`origin()`](/versions/latest/sdk/server#origin) helper method to access this value.

```ts
import { origin } from 'expo-server';

export async function GET(request: Request) {
  const target = new URL('/help', origin() ?? request.url);
  return Response.redirect('https://expo.dev', 302);
}
```

Most runtimes that you deploy your server code to have a concept of environments, to differentiate between production or staging deployments. You can use `expo-server`'s [`environment()`](/versions/latest/sdk/server#environment) helper to get an environment name. This value will differ depending on how you're running your server code.

```ts
import { environment } from 'expo-server';

export async function GET(request: Request) {
  const env = environment();
  if (env === 'staging') {
    return Response.json({ isStaging: true });
  } else if (!env) {
    return Response.json({ isProduction: true });
  } else {
    return Response.json({ env });
  }
}
```

### Task scheduling

In your request handlers, you may need to run asynchronous tasks in parallel to your server logic.

```ts
export async function GET(request: Request) {
  // This will delay the response:
  await pingAnalytics(...);

  const data = await fetchExampleData(...);
  return Response.json({ data });
}
```

In the above example, an `await`-ed function call delays the rest of the API route's execution. If we don't want to delay a `Response`, then `await`-ing this call isn't suitable. However, calling the function without `await` wouldn't guarantee that this task keeps a serverless function running.

Instead, you can use `expo-server`'s [`runTask()`](/versions/latest/sdk/server#runtaskfn) helper function to run concurrent tasks. This is equivalent to the [`waitUntil()`](https://developer.mozilla.org/en-US/docs/Web/API/ExtendableEvent/waitUntil) method that you see in service worker code or other serverless runtimes.

```ts
import { runTask } from 'expo-server';

export async function GET(request: Request) {
  // This will NOT delay the response:
  runTask(async () => {
    await pingAnalytics(...);
  });

  const data = await fetchExampleData(...);
  return Response.json({ data });
}
```

With `runTask`, you have a compromise between `await`-ing and not `await`-ing asynchronous functions. They'll be run concurrently, and don't delay the API route's response or execution, but are also making sure the runtime is aware of them, and don't quit early.

However, sometimes you may want to delay a task until after the API route has returned a `Response`. In such cases, you might prefer not to execute the task if the API has rejected it. Additionally, you may want to run a function only after a time-sensitive task has been completed to prevent concurrent code from delaying computation-heavy tasks in your API route.

You can use `expo-server`'s [`deferTask()`](/versions/latest/sdk/server#defertaskfn) helper function to schedule tasks to run after a `Response` has been resolved by your API route.

```ts
import { deferTask } from 'expo-server';

export async function GET(request: Request) {
  // This will run after this entire function resolves:
  deferTask(async () => {
    await pingAnalytics(...);
  });

  const data = await fetchExampleData(...);
  return Response.json({ data });
}
```

### Response headers

When structuring and splitting server logic into separate helper functions and files, it may be necessary to modify `Response` headers before a `Response` has been created.

For example, you may need to add metadata in [server middleware](/router/web/middleware) to a `Response` before your API route code is running.

```ts
import { setResponseHeaders } from 'expo-server';

export default function middleware(request: Request) {
  // Rate limiters typically add a `Retry-After` header
  setResponseHeaders({ 'Retry-After': '3600' });
}
```

In the above example, a `Retry-After` header is added to a future `Response` that an API route may be creating. This can also be extended for authentication and cookies.

```ts
import { setResponseHeaders } from 'expo-server';

export default function middleware(request: Request) {
  // Append cookie to future response
  setResponseHeaders(headers => {
    headers.append('Set-Cookie', 'token=123; Secure');
  });
}
```

## Bundling

API Routes are bundled with Expo CLI and [Metro bundler](/guides/customizing-metro). They have access to all of the language features as your client code:

-   [TypeScript](/guides/typescript) — types and [**tsconfig.json** paths](/guides/typescript#path-aliases-optional).
-   [Environment variables](/guides/environment-variables) — server routes have access to all environment variables, not just the ones prefixed with `EXPO_PUBLIC_`.
-   Node.js standard library — ensure that you are using the correct version of Node.js locally for your server environment.
-   **babel.config.js** and **metro.config.js** support — settings work across both client and server code.

## Security

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.

-   Client code that imports code with a secret is included in the client bundle. It applies to **all files** in the **src/app directory** even though they are not a route handler file (such as suffixed with **+api.ts**).
-   If the secret is in a **<...>+api.ts** file, it is not included in the client bundle. It applies to all files that are imported in the route handler.
-   The secret stripping takes place in `expo/metro-config` and requires it to be used in the **metro.config.js**.

## Deployment

When you're ready to deploy to production, run the following command to create the server bundle in the **dist** directory (see the [Expo CLI documentation](/more/expo-cli#exporting) for more details):

```sh
npx expo export --platform web
```

This server can be tested locally with `npx expo serve`, visit the URL in a web browser or create a native build with the `origin` set to the local server URL. You can deploy the server for production using [EAS Hosting](/eas/hosting/get-started) or another third-party service.

If you want to export API routes and skip generating a website version of your app, you can use the following command, which will generate a **dist** directory containing only the server code of your project.

```sh
npx expo export --platform web --no-ssg
```

[Deploy instantly with EAS](/eas/hosting/get-started) — EAS Hosting is the best way to deploy your Expo API routes and servers.

### Native deployment

> This is an [alpha](/more/release-statuses#alpha) feature. The process will be more automated and have better support in future versions.

Server features (API routes, and React Server Components) in Expo Router are centered around native implementations of `window.location` and `fetch` which point to the remote server. In development, we automatically point to the dev server running with `npx expo start`, but for production native builds to work you'll need to deploy the server to a secure host and set the `origin` property of the Expo Router Config Plugin.

When configured, features like relative fetch requests `fetch('/my-endpoint')` will automatically point to the server origin.

This deployment process can experimentally be automated to ensure correct versioning during native builds with the `EXPO_UNSTABLE_DEPLOY_SERVER=1` environment variable.

Here's how to configure your native app to automatically deploy and link a versioned production server on build:

Ensure the `origin` field is **NOT** set in the **app.json** or in the `expo.extra.router.origin` field. Also, ensure you aren't using **app.config.js** as this is not supported with automatically linked deployments yet.

Setup [EAS Hosting](/eas/hosting/get-started) for the project by deploying once locally first.

```sh
npx expo export -p web
eas deploy
```

Set the `EXPO_UNSTABLE_DEPLOY_SERVER` environment variable in your `.env` file. This will be used to enable the experimental server deployment functionality during EAS Build.

```sh
EXPO_UNSTABLE_DEPLOY_SERVER=1
```

You're now ready to use automatic server deployment! Run the build command to start the process.

```sh
eas build
```

You can also run this locally with:

```sh
npx expo run:android --variant release
npx expo run:ios --configuration Release
```

Notes about automatic server deployment for native apps:

-   Server failures may occur during the `Bundle JavaScript` phase of EAS Build if something was not setup correctly.
-   You can manually deploy the server and set the `origin` URL before building the app if you'd like.
-   Automatic deployment can be force skipped with the environment variable `EXPO_NO_DEPLOY=1`.
-   Automatic deployment does not support [dynamic app config](/workflow/configuration#dynamic-configuration) (**app.config.js** and **app.config.ts**) files yet.
-   Logs from the deployment will be written to `.expo/logs/deploy.log`.
-   Deployment will not run in `EXPO_OFFLINE` mode.

### Testing the native production app locally

It can often be useful to test the production build against a local dev server to ensure everything is working as expected. This can speed up the debugging process substantially.

Export the production server:

```sh
npx expo export
```

Host the production server locally:

```sh
npx expo serve
```

Set the origin in the **app.json**'s `origin` field. Ensure no generated value is in `expo.extra.router.origin`. This should be `http://localhost:8081` (assuming `npx expo serve` is running on the default port).

```json
{
  "expo": {
    "plugins": [
      [
        "expo-router",
        {
          "origin": "http://localhost:8081"
        }
      ]
    ]
  }
}
```

Remember to remove this `origin` value when deploying to production.

Build the app in release mode on to a simulator:

```sh
EXPO_NO_DEPLOY=1 npx expo run:ios --configuration Release
```

You should now see requests coming in to the local server. Use a tool like [Proxyman](https://proxyman.com/) to inspect network traffic for the simulator and gain better insight.

You can experimentally change the URL and quickly rebuild for iOS using the `--unstable-rebundle` flag. This will swap out the **app.json** and client assets for new ones, skipping the native rebuild.

For example, you can run `eas deploy` to get a new deployment URL, add it to the **app.json**, then run `npx expo run:ios --unstable-rebundle --configuration Release` to quickly rebuild the app with the new URL.

You will want to make a clean build before sending to the store to ensure no transient issues are present.

## Hosting on third-party services

> The `expo-server` library was added in SDK 54. Use `@expo/server` for older SDKs instead.

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`](/more/expo-cli#exporting) command:

-   **dist** is the default export directory for Expo CLI.
-   Files in **public** directory are copied to **dist** on export.
-   The `expo-server` package is a server-side runtime for exported Expo web and API route artifacts.
-   `expo-server` does **not** inflate environment variables from **.env** files. They are expected to load either by the hosting provider or the user.
-   Metro is not included in the server.

The `expo-server` library contains adapters for various providers and runtimes. Before proceeding with any of the below sections, install the `expo-server` library.

```sh
npx expo install expo-server
```

### Bun

Export the website for production:

```sh
bunx expo export -p web
```

Write a server entry file that serves the static files and delegates requests to the server routes:

```ts
import { createRequestHandler } from 'expo-server/adapter/bun';

const CLIENT_BUILD_DIR = `${process.cwd()}/dist/client`;
const SERVER_BUILD_DIR = `${process.cwd()}/dist/server`;
const handleRequest = createRequestHandler({ build: SERVER_BUILD_DIR });

const port = process.env.PORT || 3000;

Bun.serve({
  port: process.env.PORT || 3000,
  async fetch(req) {
    const url = new URL(req.url);
    console.log('Request URL:', url.pathname);

    const staticPath = url.pathname === '/' ? '/index.html' : url.pathname;
    const file = Bun.file(CLIENT_BUILD_DIR + staticPath);

    if (await file.exists()) return new Response(await file.arrayBuffer());

    return handleRequest(req);
  },
  websocket,
});

console.log(`Bun server running at http://localhost:${port}`);
```

Start the server with `bun`:

```sh
bun run server.ts
```

### Express

Install the required dependencies:

```sh
npm i -D express compression morgan
```

Export the website for production:

```sh
npx expo export -p web
```

Write a server entry file that serves the static files and delegates requests to the server routes:

```ts
#!/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(
  '/{*all}',
  createRequestHandler({
    build: SERVER_BUILD_DIR,
  })
);
const port = process.env.PORT || 3000;

app.listen(port, () => {
  console.log(`Express server listening on port ${port}`);
});
```

Start the server with `node` command:

```sh
node server.ts
```

### Netlify

> Third-party adapters are subject to breaking changes. We have no continuous tests against them.

Create a server entry file. All requests will be delegated through this middleware. The exact file location is important.

```ts
import path from 'node:path';
import { createRequestHandler } from 'expo-server/adapter/netlify';

export default createRequestHandler({
  build: path.join(__dirname, '../../dist/server'),
});
```

Create a Netlify configuration file at the root of your project to redirect all requests to the server function.

```yaml
[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"
```

After you have created the configuration files, you can build the website and functions with Expo CLI:

```sh
npx expo export -p web
```

Deploy to Netlify with the [Netlify CLI](https://docs.netlify.com/cli/get-started/).

```sh
npm install netlify-cli -g
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.

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.

### Vercel

> Third-party adapters are subject to breaking changes. We have no continuous tests against them.

Create a server entry file. All requests will be delegated through this middleware. The exact file location is important.

```ts
const { createRequestHandler } = require('expo-server/adapter/vercel');

module.exports = createRequestHandler({
  build: require('path').join(__dirname, '../dist/server'),
});
```

Create a Vercel configuration file (**vercel.json**) at the root of your project to redirect all requests to the server function.

```json
{
  "buildCommand": "expo export -p web",
  "outputDirectory": "dist/client",
  "functions": {
    "api/index.ts": {
      "runtime": "@vercel/node@5.1.8",
      "includeFiles": "dist/server/**"
    }
  },
  "rewrites": [
    {
      "source": "/(.*)",
      "destination": "/api/index"
    }
  ]
}
```

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.

> **Note:** This step only applies to users of the **legacy** version of the **vercel.json**. If you're using v3, you can skip this step.

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`.

Deploy to Vercel with the [Vercel CLI](https://vercel.com/docs/cli).

```sh
npm install vercel -g
vercel build
vercel deploy --prebuilt
```

You can now visit your website at the URL provided by the Vercel CLI.

## Known limitations

Several known features are not currently supported in the API Routes beta release.

### No dynamic imports

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.

### ESM not supported

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.
