Integrating Third-Party APIs in RapidNative Apps

SA

By Suraj Ahmed

13th Sep 2026

Last updated: 13th Sep 2026

Integrating Third-Party APIs in RapidNative Apps

Every mobile app worth shipping eventually needs to talk to something outside itself. A payment processor. A language model. A weather feed. A geocoder. An email sender. The moment you cross that boundary, three problems appear at once: how do you keep your API keys out of the mobile binary, how do you handle streaming responses in React Native, and how do you organize server code so a non-developer can still ship features without breaking security?

Third-party API integration in React Native apps is where most mobile projects quietly lose their footing — a hardcoded key ends up in the App Store bundle, an SSE stream fails silently on iOS, or a "quick" Supabase Edge Function turns into a two-day Deno rabbit hole. RapidNative — the AI mobile-app builder that turns natural-language prompts into production-ready React Native and Expo apps — solves these questions by baking a specific architecture into every project it generates. This guide walks through that architecture (the "gateway pattern"), the three environment-variable layers it enforces, and real code excerpts for the four API integrations most apps need: an AI chatbot, image analysis, transactional email, and Stripe.

If you've ever added a third-party API to a mobile app and later found yourself grepping the repo for a hardcoded key that shipped to production, this is for you.

Developer working on API integration code on a laptop Photo by Ilya Pavlov on Unsplash

What RapidNative actually generates

A quick grounding, because the architecture only makes sense once you see the shape of the code.

When you describe an app to RapidNative — "a fitness tracker with a workout log, streaks, and a coach chatbot" — the generator produces a monorepo with three top-level workspaces:

  • mobile/ — the Expo Router app that runs on iOS, Android, and the web. React Native 0.86, Expo SDK 57, TypeScript, TanStack Query for server state.
  • api/ — a tiny Express server (api/index.js) that owns every server-side integration. Runs on Node.
  • supabase/ — SQL migrations, seed data, and row-level security policies. Plain Postgres, no ORM.

Three services, one repo, all deployed automatically. The mobile app talks to Supabase directly for its own data (RLS enforces per-user isolation), and it talks to the Express server for everything else — every third-party API, every server-side computation, every place where a secret is involved.

That "everything else" is where this article lives.

The gateway pattern in one paragraph

The mobile app never calls a third-party API directly. It calls one address — the Express server — and the Express server does the outbound work with the credentials only it holds. If your app needs GPT-4o for a chatbot, the phone sends a message to ${API_URL}/chat; the server sends the message to the LLM provider with the real API key; the response streams back through the same channel. The phone never sees the key, doesn't know which model was picked, and can't be reverse-engineered into a free credit farm.

This is not a novel idea — it's the same pattern behind every non-trivial mobile app that touches money — but RapidNative wires it up for you and then bakes it into the AI agent's system prompt so every subsequent feature you request follows the same shape. Ask for a "GPT-powered recipe suggester" and it will scaffold a /recipes server route AND the client-side fetch in a single generation, correctly.

Network cables representing API gateway architecture Photo by Alina Grubnyak on Unsplash

Where your API keys actually live

There are three environment-variable layers, and understanding the split is the difference between shipping safely and shipping a leak. Each layer runs in a different process, has a different threat model, and is loaded from a different place.

Layer 1 — client, public. Anything prefixed EXPO_PUBLIC_* is embedded in the mobile JavaScript bundle. It ships to every user, every device, every reverse-engineering attempt. Only put things here that are safe to publish: your Supabase URL, your Supabase anon key (RLS makes it safe to expose), your API server hostname, and non-secret feature flags.

EXPO_PUBLIC_SUPABASE_URL=https://xyz.supabase.co
EXPO_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOi...
EXPO_PUBLIC_API_URL=https://api-abc123.rapidnative.app

Layer 2 — server, private. Anything read by api/index.js via process.env runs on the server only. Third-party API keys, service-role database keys, webhook secrets, and any credential you'd panic about seeing on Twitter go here. These never appear in the mobile bundle.

STRIPE_SECRET_KEY=sk_live_...
OPENAI_API_KEY=sk-...
RESEND_API_KEY=re_...
SUPABASE_SERVICE_ROLE_KEY=eyJ...

Layer 3 — platform, injected. RapidNative's deployment layer auto-injects a small set of environment variables at boot so services can find each other. EXPO_PUBLIC_API_URL on the phone always points to the current deployment of api/; SUPABASE_URL on the server always points to the current database. You never hand-wire these — the orchd.json in the project root does it for you.

The rule to remember: anything starting with EXPO_PUBLIC_ ships to users; nothing else does. If you accidentally read a server-only secret from the mobile app, it will be undefined at runtime. That's a pit of success — the system fails loudly instead of silently leaking.

Writing your first API route

Here's a simplified version of the api/index.js template every fullstack RapidNative project starts with:

import express from 'express';
import 'dotenv/config';

const app = express();
app.use(express.json());

// Reusable helper for the platform's brokered services
async function callService(path, body) {
  return fetch(`${process.env.RAPIDNATIVE_GLOBAL_SERVICES_URL}${path}`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.RAPIDNATIVE_GLOBAL_SERVICES_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(body),
  });
}

// Every api/ MUST expose /health for the platform's readiness check
app.get('/health', (req, res) => res.json({ status: 'ok' }));

// Example: brokered AI chat endpoint
app.post('/chat', async (req, res) => {
  try {
    const { messages, model } = req.body;
    const response = await callService('/openrouter/v1/chat/completions', {
      model: model || 'openai/gpt-4o-mini',
      messages,
    });
    const data = await response.json();
    res.status(response.status).json(data);
  } catch (err) {
    console.error('Chat error:', err);
    res.status(500).json({ error: 'Chat request failed' });
  }
});

const port = process.env.PORT || 3000;
app.listen(port, () => console.log(`api listening on :${port}`));

A few non-obvious rules baked into that template:

  • /health is not optional. The deployment layer polls it to decide whether the service is ready to receive traffic. Delete it and your API silently 502s.
  • process.env.PORT is required. Hardcoding a port breaks reproducibility across environments.
  • Errors are logged AND returned. Silent failures are the worst class of bug in this stack; the mobile app has no way to read server logs.
  • callService is a helper, not a one-off. Every brokered integration (LLM, email, Google auth) reuses it. Copy-paste is the enemy of correctness.

Calling the route from a mobile screen

The client side is deliberately unremarkable — just fetch against the injected EXPO_PUBLIC_API_URL:

const API_URL = process.env.EXPO_PUBLIC_API_URL;

async function sendMessage(text: string) {
  const response = await fetch(`${API_URL}/chat`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      messages: [{ role: 'user', content: text }],
    }),
  });
  if (!response.ok) throw new Error(`chat failed: ${response.status}`);
  const data = await response.json();
  return data.choices?.[0]?.message?.content ?? '';
}

Wrap that in a useMutation from React Query (which ships in the template), show a loading state, and you have a working chat feature. The mobile side has no idea whether the server used GPT-4o, Claude, or a local Llama — it just sees a message come back.

Person using a mobile phone with a chat interface Photo by Jamie Street on Unsplash

Streaming: the one React Native gotcha that will bite you

If you asked a code assistant to help you stream tokens from an LLM into a React Native app, it would confidently tell you to iterate response.body.getReader(). It would be wrong. In React Native, the global fetch's response body is null — always — because React Native's networking layer doesn't expose the underlying readable stream. Your app will crash with "Cannot read properties of null (reading 'getReader')" and it will be hard to Google, because the same code works everywhere else.

The fix is a one-line import change:

import { fetch as expoFetch } from 'expo/fetch';

async function streamChat(text: string, onChunk: (t: string) => void) {
  const response = await expoFetch(`${API_URL}/chat/stream`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      messages: [{ role: 'user', content: text }],
    }),
  });

  const reader = response.body!.getReader();
  const decoder = new TextDecoder();

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    const chunk = decoder.decode(value);
    for (const line of chunk.split('\n')) {
      if (!line.startsWith('data: ')) continue;
      const payload = line.slice(6).trim();
      if (payload === '[DONE]') return;
      const parsed = JSON.parse(payload);
      const delta = parsed.choices?.[0]?.delta?.content;
      if (delta) onChunk(delta);
    }
  }
}

expo/fetch exposes a real WHATWG-compliant body stream, which is what you need for Server-Sent Events, chunked responses, and progressive JSON. Import it only in the files that stream — no reason to add the polyfill everywhere.

On the server, the streaming route pipes the upstream SSE response straight to the client:

app.post('/chat/stream', async (req, res) => {
  const upstream = await callService('/openrouter/v1/chat/completions', {
    ...req.body,
    stream: true,
  });
  res.setHeader('Content-Type', 'text/event-stream');
  upstream.body.pipe(res);
});

Twenty lines total for a streaming AI chat. That's the payoff of the gateway pattern.

Vision AI without leaking user photos

Multimodal (image + text) endpoints follow the same shape with two twists: images are base64-encoded on the phone, and the server must NOT specify a model — the broker auto-detects image_url parts and routes to a vision-capable model.

import * as ImagePicker from 'expo-image-picker';

async function analyzeImage(prompt: string) {
  const result = await ImagePicker.launchImageLibraryAsync({
    mediaTypes: ['images'],
    base64: true,
    quality: 0.5, // keep the payload sane
  });
  if (result.canceled || !result.assets[0].base64) return null;

  const base64Uri = `data:image/jpeg;base64,${result.assets[0].base64}`;

  const response = await fetch(`${API_URL}/vision`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      messages: [{
        role: 'user',
        content: [
          { type: 'text', text: prompt },
          { type: 'image_url', image_url: { url: base64Uri } },
        ],
      }],
    }),
  });
  const data = await response.json();
  return data.choices?.[0]?.message?.content;
}

Server side:

app.post('/vision', async (req, res) => {
  const response = await callService('/openrouter/v1/chat/completions', {
    messages: req.body.messages, // no model — broker picks a vision model
  });
  res.status(response.status).json(await response.json());
});

The photo never leaves the request-response cycle. It's not written to disk on the API server, not stored, not logged. Good default for privacy, and it makes the server stateless — you can horizontally scale api/ without worrying about session affinity.

Bringing your own API: a Stripe example

The brokered services (LLMs, transactional email, Google auth) are pre-wired. Everything else — Stripe, Twilio, Google Maps, your own microservice, a bespoke ML model — you install and wire yourself, following the exact same pattern.

Say you want to accept subscriptions. Three steps:

1. Add the SDK to api/package.json:

{
  "dependencies": {
    "express": "^4.21.0",
    "dotenv": "^16.4.0",
    "stripe": "^17.0.0"
  }
}

2. Store STRIPE_SECRET_KEY as a server-side environment variable in the RapidNative project settings — never commit it, never prefix it with EXPO_PUBLIC_.

3. Add a checkout route to api/index.js:

import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

app.post('/checkout', async (req, res) => {
  try {
    const { priceId, userId } = req.body;
    const session = await stripe.checkout.sessions.create({
      mode: 'subscription',
      line_items: [{ price: priceId, quantity: 1 }],
      success_url: `${process.env.APP_URL}/success`,
      cancel_url: `${process.env.APP_URL}/cancel`,
      client_reference_id: userId,
    });
    res.json({ url: session.url });
  } catch (err) {
    console.error('Checkout error:', err);
    res.status(500).json({ error: 'Checkout failed' });
  }
});

The mobile app opens session.url in an in-app browser (expo-web-browser) and Stripe handles the rest. When payment completes, Stripe posts a webhook to a /webhook/stripe route on the same Express server, which updates the database with the service-role key. Same shape, same file, same helper — the pattern scales.

The same recipe works for Twilio (SMS via twilio.messages.create), SendGrid, Google Places (server-proxied to hide the key), or your own internal microservice.

Secure lock and code representing API security Photo by FLY:D on Unsplash

What NOT to do (real mistakes people make)

Four common failure modes, each of which the gateway pattern is designed to make impossible.

1. Calling a third-party API directly from the mobile app. Even for "just" a weather API with a free tier. The moment your app is public, your key is scraped from the JS bundle, your quota is drained, and your account is billed. There is no such thing as a rate-limited free tier that survives contact with an unrestricted mobile client.

2. Prefixing a secret with EXPO_PUBLIC_. The prefix is a flare that says "safe to publish." If you rename STRIPE_SECRET_KEY to EXPO_PUBLIC_STRIPE_SECRET_KEY because the mobile app couldn't read it, congratulations: you've just published your Stripe secret to every user of your app. Undo the rename and add a server route instead.

3. Expecting Supabase Edge Functions to work. They don't, on this stack. supabase.functions.invoke(...) returns a 404 in preview and in production. If you're pattern-matching from a Supabase tutorial, mentally substitute "Edge Function" with "Express route in api/index.js" and everything else works.

4. Trying to stream with global fetch. Covered above, but worth repeating because it's the single most common frustration when adding an AI chat feature. import { fetch } from 'expo/fetch' — one line, saves an afternoon.

When to add a new endpoint

A mental model: if the operation involves a secret, a webhook, or a computation that shouldn't be trusted to the client, it belongs in api/index.js. If it's pure database CRUD on rows the current user owns, it belongs in a Supabase query directly from the mobile side, because RLS is doing the security work for you.

OperationWhere it goes
Read the current user's own workoutsMobile → Supabase (RLS enforces access)
Send a transactional emailMobile → api/send-email → Resend
Charge a cardMobile → api/checkout → Stripe
Delete a user's account (all their data)Mobile → api/delete-account → Supabase service role
Ask an LLM to summarize a messageMobile → api/chat → brokered LLM
Filter a list on-deviceMobile only — no round-trip

This is the instinct a senior mobile engineer develops over a couple of years. RapidNative gives you the scaffolding on day one.

FAQ

How do I add my own API key to a RapidNative project? Open your project settings, add the key as a server-side environment variable (do not prefix it with EXPO_PUBLIC_), then reference it in api/index.js via process.env.YOUR_KEY. The key never touches the mobile bundle.

Can I call any third-party API from a RapidNative app? Yes — any REST or GraphQL API works. Add the SDK (or use fetch) in api/index.js, expose a route to the mobile app, and call it from a screen. The gateway pattern is the constraint, not which APIs you can use.

Do I need to use Supabase Edge Functions? No, and you shouldn't on this stack. RapidNative's fullstack template intentionally uses an Express server in api/ instead of Edge Functions, because it's easier to debug, faster to iterate, and consistent between local dev and production.

How do I handle streaming responses from an AI API? Use fetch from expo/fetch, not the global fetch. The global one returns a null body on React Native; expo/fetch gives you a real readable stream you can iterate with getReader().

Can non-developers add API integrations, or do I need to write code? Describe the integration to the RapidNative agent in plain English ("add a Stripe checkout for a $19 monthly plan"), and it will write the server route and the client call following the gateway pattern. You review the generated code and add the API key.

Ship it

The gateway pattern isn't unique to RapidNative — it's what every professional mobile team eventually converges on — but building it from scratch takes days of infrastructure work. RapidNative generates it in the first two minutes of your first prompt, wires the environment variables through orchd, and teaches its agent to keep every future feature inside the pattern.

If you want to try it, start a new project — you get 20 free credits, no credit card. Prompt it with "an AI recipe app that emails weekly meal plans" and watch it scaffold a chat endpoint, an email endpoint, and a Supabase schema, all wired through the gateway you now know intimately.

Further reading on the RapidNative blog: How RapidNative generates a full-stack backend from a single prompt, the Supabase self-hosted primer, and the Stripe subscription API guide. External references worth bookmarking: the Expo networking docs, the React Native Directory for well-vetted community libraries, and the Stripe API reference .

Start now

Ready to build your app?

Turn your idea into a production-ready React Native app in minutes.

Free tools to get you started

Questions

Frequently asked questions

What is RapidNative?

RapidNative is an AI-powered mobile app builder. Describe the app you want in plain English and RapidNative generates real, production-ready React Native screens you can preview, edit, and publish to the App Store or Google Play.

Can I export the code?

Yes. RapidNative generates clean React Native and Expo code that you can export at any time. No lock-in, no proprietary format. Hand it to your developers or keep building inside RapidNative.

Is RapidNative free to use?

Yes. You can build apps on the free plan with no credit card required. Paid plans unlock unlimited AI generations, code export, and direct publishing to the App Store and Google Play.

Do I need to know how to code?

No. Most users build apps by describing what they want in plain English. Developers can drop into the code whenever they want more control, but coding is optional.

How long does it take to build an app?

Most users have a working first screen in under a minute. A full MVP usually takes a few hours instead of the weeks or months traditional development requires.