1--- 2title: API Routes 3description: Learn how to create server functions with Expo Router. 4--- 5 6> **warning** This feature is still experimental. Available from Expo SDK 50 and Expo Router v3. 7 8import { FileTree } from '~/ui/components/FileTree'; 9import { Step } from '~/ui/components/Step'; 10import { Terminal } from '~/ui/components/Snippet'; 11import { Collapsible } from '~/ui/components/Collapsible'; 12 13Expo Router enables you to write server code for all platforms, right in your **app** directory. 14 15```json app.json 16{ 17 "web": { 18 "bundler": "metro", 19 /* @info Output a dynamic server. */ 20 "output": "server" 21 /* @end */ 22 } 23} 24``` 25 26Server features require a custom Node.js server. Most hosting providers support Node.js, including [Netlify](#netlify), [Cloudflare](https://www.cloudflare.com/), and [Vercel](https://vercel.com). 27 28## What are API Routes 29 30API 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](https://wintercg.org/)-compliant environment. 31 32API 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. 33 34<FileTree files={['app/index.js', ['app/hello+api.ts', 'API Route']]} /> 35 36## Create an API route 37 38<Step label="1"> 39 40An API route is created in the **app** directory. For example, add the following route handler. It is executed when the route `/hello` is matched. 41 42```js app/hello+api.ts 43import { ExpoRequest, ExpoResponse } from 'expo-router/server'; 44 45export function GET(request: ExpoRequest) { 46 return ExpoResponse.json({ hello: 'world' }); 47} 48``` 49 50You 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`. 51 52</Step> 53 54<Step label="2"> 55 56Start the development server with Expo CLI: 57 58<Terminal cmd={['$ npx expo']} /> 59 60</Step> 61 62<Step label="3"> 63 64You can make a network request to the route to access the data. Run the following command to test the route: 65 66<Terminal cmd={['$ curl http://localhost:8081/hello']} /> 67 68You can also make a request from the client code: 69 70```js app/index.js 71import { Button } from 'react-native'; 72 73async function fetchHello() { 74 const response = await fetch('/hello'); 75 const data = await response.json(); 76 alert('Hello ' + data.hello); 77} 78 79export default function App() { 80 return <Button onPress={() => fetchHello()} title="Fetch hello" />; 81} 82``` 83 84This 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: 85 86```json app.json 87{ 88 "plugins": [ 89 [ 90 "expo-router", 91 { 92 /* @info The URL where the API routes are hosted. */ 93 "origin": "https://evanbacon.dev/" 94 /* @end */ 95 } 96 ] 97 ] 98} 99``` 100 101</Step> 102 103<Step label="4"> 104 105Deploy the website and server to a [hosting provider](#deployment) to access the routes in production on both native and web. 106 107</Step> 108 109## Requests 110 111Requests use the global `ExpoRequest` object. It is a subclass of the standard [`Request`](https://fetch.spec.whatwg.org/#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. 112 113```ts app/blog/[post]+api.ts 114import { ExpoRequest, ExpoResponse } from 'expo-router/server'; 115 116export async function GET(request: ExpoRequest, { post }: Record<string, string>) { 117 // const postId = request.expoUrl.searchParams.get('post') 118 // fetch data for `post` 119 return ExpoResponse.json({ ... }); 120} 121``` 122 123### Request body 124 125Use the `request.json()` function to access the request body. It automatically parses the body and returns the result. 126 127```ts app/validate+api.ts 128import { ExpoRequest, ExpoResponse } from 'expo-router/server'; 129 130export async function POST(request: ExpoRequest) { 131 const body = await request.json(); 132 133 return ExpoResponse.json({ ... }); 134} 135``` 136 137## Response 138 139Responses use the global `ExpoResponse` object. It is a subclass of the standard [`Response`](https://fetch.spec.whatwg.org/#response) object and provides additional functionality such as the `ExpoResponse.json` — an initializer which automatically stringifies the response body and returns status `200`. 140 141```ts app/demo+api.ts 142import { ExpoRequest, ExpoResponse } from 'expo-router/server'; 143 144export function GET() { 145 return ExpoResponse.json({ hello: 'universe' }); 146} 147``` 148 149## Errors 150 151You can respond to server errors by using the `ExpoResponse` object. 152 153```ts app/blog/[post].ts 154import { ExpoRequest, ExpoResponse } from 'expo-router/server'; 155 156export async function GET(request: ExpoRequest, { post }: Record<string, string>) { 157 if (!post) { 158 return new ExpoResponse('No post found', { 159 status: 404, 160 headers: { 161 'Content-Type': 'text/plain', 162 }, 163 }); 164 } 165 // fetch data for `post` 166 return ExpoResponse.json({ ... }); 167} 168``` 169 170Making 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`. 171 172## Bundling 173 174API 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: 175 176- [TypeScript](/guides/typescript) — types and [**tsconfig.json** paths](/guides/typescript#path-aliases). 177- [Environment variables](/guides/environment-variables) — server routes have access to all environment variables, not just the ones prefixed with `EXPO_PUBLIC_`. 178- Node.js standard library — ensure that you are using the correct version of Node.js locally for your server environment. 179- **babel.config.js** and **metro.config.js** support — settings work across both client and server code. 180 181{/* TODO: SSR, redirects, middleware, and so on. */} 182 183## Security 184 185> **warning** While in beta, server code may leak into the client bundle. This won't happen in the stable release. 186 187Route 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. 188 189- Client code that imports code with a secret is included in the client bundle. It applies to **all files** in the **app directory** even though they are not a route handler file (such as suffixed with **+api.js**). 190- If the secret is in a **<...>+api.js** file, it is not included in the client bundle. It applies to all files that are imported in the route handler. 191- The secret stripping takes place in `expo/metro-config` and requires it to be used in the **metro.config.js**. 192 193## Deployment 194 195> **warning** This is experimental and subject to breaking changes. We have no continuous tests against this configuration. 196 197Every 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. 198 199Before deploying to these providers, it may be good to be familiar with the basics of [`npx expo export`](/more/expo-cli#exporting) command: 200 201- **/dist** is the default export directory for Expo CLI. 202- Files in **/public** are copied to **/dist** on export. 203- The `@expo/server` package is included with `expo` and delegates requests to the server routes. 204- `@expo/server` does **not** inflate environment variables from **.env** files. They are expected to load either by the hosting provider or the user. 205- Metro is not included in the server. 206 207### Express 208 209<Step label="1"> 210 211Install the required dependencies: 212 213<Terminal cmd={['$ npm i -D express compression morgan']} /> 214 215</Step> 216 217<Step label="2"> 218 219Export the website for production: 220 221<Terminal cmd={['$ npx expo export -p web']} /> 222 223</Step> 224 225<Step label="3"> 226 227Write a server entry file that serves the static files and delegates requests to the server routes: 228 229```js server.js 230#!/usr/bin/env node 231 232const path = require('path'); 233const { createRequestHandler } = require('@expo/server/adapter/express'); 234 235const express = require('express'); 236const compression = require('compression'); 237const morgan = require('morgan'); 238 239const BUILD_DIR = path.join(process.cwd(), 'dist'); 240 241const app = express(); 242 243app.use(compression()); 244 245// http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header 246app.disable('x-powered-by'); 247 248process.env.NODE_ENV = 'production'; 249 250app.use( 251 // Prevent access to expo functions as these may 252 // contain sensitive information. 253 [/^\/_expo\/functions($|\/)/, '/'], 254 express.static(BUILD_DIR, { 255 maxAge: '1h', 256 extensions: ['html'], 257 }) 258); 259 260app.use(morgan('tiny')); 261 262app.all( 263 '*', 264 createRequestHandler({ 265 build: BUILD_DIR, 266 }) 267); 268const port = process.env.PORT || 3000; 269 270app.listen(port, () => { 271 console.log(`Express server listening on port ${port}`); 272}); 273``` 274 275</Step> 276 277<Step label="4"> 278 279Start the server with `node` command: 280 281<Terminal cmd={['$ node server.js']} /> 282 283</Step> 284 285### Netlify 286 287> **warning** This is experimental and subject to breaking changes. We have no continuous tests against this configuration. 288 289<Step label="1"> 290 291Create a server entry file. All requests will be delegated through this middleware. The exact file location is important. 292 293```js netlify/functions/server.js 294const { createRequestHandler } = require('@expo/server/adapter/netlify'); 295 296const handler = createRequestHandler({ 297 /* @info Points to the root `dist/` (output) folder */ 298 build: require('path').join(__dirname, '../../dist'), 299 /* @end */ 300 mode: process.env.NODE_ENV, 301}); 302 303module.exports = { handler }; 304``` 305 306</Step> 307 308<Step label="2"> 309 310Create a Netlify configuration file at the root of your project to redirect all requests to the server function. 311 312```yaml netlify.toml 313[build] 314 command = "expo export -p web" 315 functions = "netlify/functions" 316 publish = "dist" 317 318[[redirects]] 319 from = "/*" 320 to = "/.netlify/functions/server" 321 status = 404 322 323[functions] 324 # Include everything to ensure dynamic routes can be used. 325 included_files = ["dist/**/*"] 326 327[[headers]] 328 for = "/dist/_expo/functions/*" 329 [headers.values] 330 # Set to 60 seconds as an example. 331 "Cache-Control" = "public, max-age=60, s-maxage=60" 332``` 333 334</Step> 335 336<Step label="3"> 337 338After you have created the configuration files, you can build the website and functions with Expo CLI: 339 340<Terminal cmd={['$ npx expo export -p web']} /> 341 342</Step> 343 344<Step label="4"> 345 346Deploy to Netlify with the [Netlify CLI](https://docs.netlify.com/cli/get-started/). 347 348<Terminal 349 cmd={[ 350 '# Install the Netlify CLI globally if needed.', 351 '$ npm install netlify-cli -g', 352 '# Deploy the website.', 353 '$ netlify deploy', 354 ]} 355/> 356 357You can now visit your website at the URL provided by Netlify CLI. Running `netlify deploy --prod` will publish to the production URL. 358 359</Step> 360 361<Step label="5"> 362 363If 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. 364 365</Step> 366