API Routes & System Prompts

How to create robust Expo API routes with expert-level system prompts for your app's AI features. All API routes run server-side — API keys stay secure and never reach the client.

API keys set in your .env file (without the EXPO_PUBLIC_ prefix) are only accessible in API route files. They are never bundled into the client app.

Route Architecture

src/app/api/
src/app/api/
  ├── [feature]+api.ts     ← Your app's core AI feature
  ├── vision+api.ts        ← Image analysis (boilerplate)
  ├── chat+api.ts          ← General AI chat (boilerplate)
  ├── generate+api.ts      ← Text/image generation (boilerplate)
  ├── transcribe+api.ts    ← Audio transcription (boilerplate)
  └── health+api.ts        ← Health check endpoint

Your custom route is the [feature]+api.ts file — this is where your app's unique AI functionality lives with its expert system prompt.

API Route Template

src/app/api/[your-feature]+api.ts
const API_KEY = process.env.OPENAI_API_KEY; // or ANTHROPIC_API_KEY

interface FeatureRequest {
  inputText: string;
  mode?: string;
  options?: Record<string, unknown>;
}

const SYSTEM_PROMPT = `You are [EXPERT ROLE] specializing in [DOMAIN].

Your Role:
- [Primary responsibility]
- [Secondary responsibility]
- [Constraints and guidelines]

Output Rules:
- You MUST respond with ONLY valid JSON
- [Specific formatting rules]

You MUST respond with ONLY valid JSON in this exact format:
{
  "result": "string — the main output",
  "confidence": 0.0-1.0,
  "details": { ... },
  "disclaimer": "string — if applicable"
}`;

export async function POST(request: Request): Promise<Response> {
  try {
    const body: FeatureRequest = await request.json();
    const { inputText } = body;

    if (!inputText?.trim()) {
      return Response.json({ error: "Input text is required" }, { status: 400 });
    }

    if (!API_KEY) {
      return Response.json({ error: "API key not configured" }, { status: 500 });
    }

    const response = await fetch("https://api.openai.com/v1/chat/completions", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${API_KEY}`,
      },
      body: JSON.stringify({
        model: "gpt-4o-mini",
        messages: [
          { role: "system", content: SYSTEM_PROMPT },
          { role: "user", content: inputText },
        ],
        response_format: { type: "json_object" },
        temperature: 0.7,
        max_tokens: 2000,
      }),
    });

    const data = await response.json();
    const result = JSON.parse(data.choices[0].message.content);
    return Response.json(result);
  } catch (error) {
    console.error("[API] Error:", error);
    return Response.json({ error: "Internal server error" }, { status: 500 });
  }
}

Anthropic (Claude) Variant

const response = await fetch("https://api.anthropic.com/v1/messages", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "x-api-key": process.env.ANTHROPIC_API_KEY!,
    "anthropic-version": "2023-06-01",
  },
  body: JSON.stringify({
    model: "claude-sonnet-4-20250514",
    max_tokens: 2000,
    system: SYSTEM_PROMPT,
    messages: [{ role: "user", content: inputText }],
  }),
});

const data = await response.json();
const result = JSON.parse(data.content[0].text);

Vision (Image Analysis) Variant

For camera/identifier apps — send base64 image to GPT-4o:

const response = await fetch("https://api.openai.com/v1/chat/completions", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${API_KEY}`,
  },
  body: JSON.stringify({
    model: "gpt-4o",
    messages: [
      { role: "system", content: SYSTEM_PROMPT },
      {
        role: "user",
        content: [
          { type: "text", text: userPrompt },
          {
            type: "image_url",
            image_url: { url: `data:image/jpeg;base64,${base64Image}` },
          },
        ],
      },
    ],
    response_format: { type: "json_object" },
    max_tokens: 2000,
  }),
});

Health Check Endpoint

Always include a health check route to verify your API routes are deployed and API keys are configured. This is the first thing to test after eas deploy.

src/app/api/health+api.ts
export function GET() {
  const hasOpenAI = !!process.env.OPENAI_API_KEY;
  const hasAnthropic = !!process.env.ANTHROPIC_API_KEY;

  return Response.json({
    status: "ok",
    timestamp: new Date().toISOString(),
    keys: {
      openai: hasOpenAI ? "configured" : "MISSING",
      anthropic: hasAnthropic ? "configured" : "MISSING",
    },
  });
}
# After deploying, verify:
curl https://your-app.expo.app/api/health

Input Truncation Guard

Prevent runaway API costs by truncating long user input before sending to the AI provider. This is especially important for text tools where users can paste large amounts of text.

const MAX_INPUT_LENGTH = 5000; // characters

export async function POST(request: Request): Promise<Response> {
  const body = await request.json();
  let { inputText } = body;

  if (!inputText?.trim()) {
    return Response.json({ error: "Input text is required" }, { status: 400 });
  }

  // Truncate to prevent excessive API costs
  if (inputText.length > MAX_INPUT_LENGTH) {
    inputText = inputText.slice(0, MAX_INPUT_LENGTH);
  }

  // ... rest of API call
}

Environment Variables

.env
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...

These are server-side only — no EXPO_PUBLIC_ prefix. They are only accessible in API routes (src/app/api/), never in client code.

EAS Hosting: API keys for deployed API routes must be set as PLAINTEXT env vars in EAS, not SECRET. Secrets are only available during EAS builds (native compile time), not at runtime in the deployed worker. See the EAS Deploy docs for details.

System Prompt Examples

Text Rewriter

You are an expert linguistic transformer specializing in text rewriting and paraphrasing.

Your Role:
- Rewrite the given text in the specified mode while preserving the original meaning
- Maintain the same language as the input
- Never add commentary — return ONLY the rewritten text

Modes: FORMAL, CASUAL, ACADEMIC, CREATIVE, SIMPLIFY, EXPAND, SUMMARIZE

You MUST respond with ONLY valid JSON:
{"rewrittenText": "...", "mode": "...", "wordCount": 0}

Fish/Snake Identifier (Camera App)

You are Fishify's marine biologist AI. You identify fish species from photographs.

Identification Methodology:
1. Body shape analysis
2. Coloration and pattern
3. Fin configuration
4. Scale type and pattern

Confidence Guide:
- 90-100%: Distinctive species with clear features visible
- 70-89%: Strong match but some features obscured
- Below 50%: Insufficient detail

You MUST respond with ONLY valid JSON:
{
  "species": "Common Name",
  "scientificName": "Genus species",
  "confidence": 0.0-1.0,
  "description": "Brief description",
  "funFact": "Interesting fact"
}

Model Selection Guide

ProviderModelBest ForCost
OpenAIgpt-4o-miniMost text tasks, fast, cheap$0.15/1M input
OpenAIgpt-4oVision/image analysis, complex reasoning$2.50/1M input
Anthropicclaude-sonnet-4Long-form writing, nuanced text$3/1M input

Quick Rules

Identifier apps (camera): use gpt-4o for vision. Text tools (rewrite, detect): use gpt-4o-mini or Claude Sonnet. Creative writing: use Claude for better prose. Simple utilities: use gpt-4o-mini for speed.

System Prompt Quality Checklist

Before shipping, verify your system prompt includes:

ElementExample
Role defined"You are [expert] specializing in [domain]"
Methodology describedStep-by-step how AI should approach the task
Output format exactJSON schema with field types and descriptions
Confidence scoringWhen should the AI be more/less confident?
Safety disclaimersMedical, legal, financial — where applicable
200+ words minimumShort prompts produce generic results
Prompts Already Written

Expert system prompts for 10+ app categories

Proven prompts for text tools, camera/identifier apps, and utilities — all included.

OpenAI & Anthropic ready
JSON-first output format
Vision variant included
Get ShipReactNative
Save 40+ hours of setup