◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-07-18Advanced

Stop Spinning Up a Separate Server: Colocate Your Backend With Expo Router API Routes

Standing up yet another Worker just to receive a RevenueCat webhook gets old fast. Expo Router API Routes let you put the backend inside the same repo as your app. Here is the implementation, from signature verification to picking a deployment target.

Expo Router6API RoutesBackend5RevenueCat30Webhook3

✦ Premium Article

Last week I was getting my seventh small app ready to ship, and I stopped mid-task. To receive a RevenueCat webhook, I was about to spin up another Cloudflare Worker, set its environment variables, note the deploy URL, and paste it into a dashboard. I had done this exact dance so many times that my hands knew it before my brain did.

Workers are a fine tool. But every new app meant two new repositories: the app itself, and a tiny server that existed only to serve that app. Versions drifted. I forgot which secret lived where. I would deploy one side and not the other, and things quietly fell out of sync.

That is when I finally took Expo Router's API Routes seriously — and the friction changed. You put the backend in the same repository as the app, under app/api/. This article walks through that implementation using a RevenueCat webhook as the working example, including signature verification and how to choose where to deploy.

The Invisible Cost of One More Standalone Worker

Let me be honest about why I switched to colocating.

As an indie developer running iOS and Android apps for years, the server-side work I actually need tends to be small. Verify an AdMob rewarded SSV callback. Receive a RevenueCat or App Store webhook and write it to my own database. Put a single proxy in front of an API so the key never ships to the client. Each of these is a few dozen lines.

For those few dozen lines, I was provisioning a separate repo and deploy pipeline every single time. Stacked across every app I ship, the cost is not the amount of code — it is the number of things to manage. I had small Workers scattered across six apps, and I could not tell which belonged to which without opening the README.

API Routes are a way to reduce that count. One repo per app. The backend lives inside it.

API Routes Are a Backend That Lives Inside the App

Expo Router API Routes work like this: put a file with the +api.ts suffix inside your app/ directory, and it becomes an endpoint that runs on the server. It is the same file-based routing convention you already use for screens, extended to the server side.

Create app/api/hello+api.ts, and you get an endpoint you can call at /api/hello. Inside, it is a function that takes a standard Web Request and returns a Response. If you have touched Next.js Route Handlers, this will feel almost identical.

There is one prerequisite that trips people up. API Routes need a server runtime, so you must set web.output to "server" in app.json. Miss this, and the build succeeds while every endpoint returns a 404 — a frustrating state to debug because nothing looks broken.

// app.json
{
  "expo": {
    "web": {
      "output": "server"
    }
  }
}
✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦Receive RevenueCat webhooks with a single app/api/webhook+api.ts file (copy-paste TypeScript included)
✦Signature verification, where to store secrets, and an EAS Hosting vs. Vercel comparison in one table
✦The exact rule I use across 6 apps to decide between a colocated route and a standalone Worker
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

⬡ Dev Tools2026-04-29
Building a Real-Time Collaborative App Backend with Rork and Cloudflare Durable Objects — Full Implementation Guide
A production-grade walkthrough for adding a self-hosted real-time collaboration backend to your Rork app using Cloudflare Durable Objects. Covers WebSocket lifecycle, hibernation-aware sessions, optimistic updates from React Native, and cost-aware design patterns — without depending on Liveblocks or Yjs hosting.
⬡ Dev Tools2026-10-15
When EXPO_PUBLIC_ Values Read Fine in Dev but Come Back undefined in Production: Telling the Spellings Apart
The API URL reads fine while developing and turns undefined once the app is built. The cause was how process.env was written. I lined up babel-preset-expo's output for dev and production and tabulated which spellings get replaced and which are left behind.
⬡ Dev Tools2026-10-14
My App Open Ad Only Showed When the App Came Back From Background. The 'Returned to Foreground' Signal Fired Before the Ad Had Loaded
On a cold start, the app open ad in my Android app never showed. The foreground signal came before the load began. Here is how I split the signal from the load and rebuilt it as logic you can test ordering by ordering.