React Native · Expo · Mobile API · 2026

CMS for React Native

ElmapiCMS is a self-hosted headless CMS with a REST API and official JavaScript SDK. This guide shows the production-safe mobile pattern: keep CMS credentials on your backend, expose app-ready endpoints, cache in React Native, and use webhooks to trigger content sync workflows.

Why

A good fit for mobile apps

Mobile teams often need to ship copy, onboarding content, support pages, and campaign blocks without waiting for app store review. ElmapiCMS provides that dynamic content layer through a predictable API.

Because it is self-hosted and project-scoped, you can run one CMS instance for multiple apps and environments while keeping access control in your backend.

If you are also shipping a web frontend, pair this guide with CMS for Next.js so both channels share one content backend.

Architecture

Use a secure API gateway pattern

For React Native, the recommended flow is App -> Your API -> ElmapiCMS. This keeps privileged tokens off devices and lets you shape responses for mobile performance.

  • Backend owns credentials: store ELMAPI_API_KEY only in server env.
  • App calls your endpoints: use app auth, rate limiting, and response shaping.
  • CMS remains the source of truth: editors publish once and all clients consume the same content.
Setup

Install SDK and configure environment variables

Install the official SDK in your backend service:

npm install @elmapicms/js-sdk

Use separate environment variables for backend and app runtime:

# Backend service env
ELMAPI_API_URL=https://your-cms.example.com/api
ELMAPI_PROJECT_ID=your-project-uuid
ELMAPI_API_KEY=your-server-token
ELMAPI_WEBHOOK_SECRET=your-webhook-secret
 
# React Native app env
EXPO_PUBLIC_API_BASE_URL=https://api.yourapp.com

Generate Project IDs and API tokens from Settings → API Access. For mobile delivery, use backend-scoped tokens and never embed them in app binaries.

Backend

Create a shared CMS client and app-facing routes

Start with one backend utility that initializes the SDK. Keep all error handling and configuration in this layer.

// backend/lib/elmapi.ts
import { createClient } from "@elmapicms/js-sdk";
 
export function cms() {
  const url = process.env.ELMAPI_API_URL;
  const key = process.env.ELMAPI_API_KEY;
  const projectId = process.env.ELMAPI_PROJECT_ID;
 
  if (!url || !key || !projectId) {
    throw new Error("Missing ElmapiCMS environment variables");
  }
 
  return createClient(url, key, projectId);
}

Mobile feed endpoint

Query only the fields needed by the app and return a lean payload for fast rendering.

// backend/routes/mobile-feed.ts (Express example)
import { Router } from "express";
import { cms } from "../lib/elmapi";
 
const router = Router();
 
router.get("/mobile/feed", async (_req, res, next) => {
  try {
    const result = await cms().getEntries("mobile_feed", {
      state: "published",
      sort: "created_at:desc",
      paginate: 20,
      page: 1,
      fields: ["title", "summary", "hero_image", "cta_url", "updated_at"],
    });
 
    const data = Array.isArray(result) ? result : result.data;
    res.json({ items: data });
  } catch (error) {
    next(error);
  }
});
 
export default router;
App Layer

Consume content with React Query

Use React Query to cache network responses and reduce repeated calls while users navigate between screens.

// app/src/features/feed/useFeed.ts
import { useQuery } from "@tanstack/react-query";
 
const API_BASE_URL = process.env.EXPO_PUBLIC_API_BASE_URL;
 
async function fetchFeed() {
  const response = await fetch(`${API_BASE_URL}/mobile/feed`);
  if (!response.ok) {
    throw new Error("Failed to fetch feed");
  }
  return response.json();
}
 
export function useFeed() {
  return useQuery({
    queryKey: ["mobile-feed"],
    queryFn: fetchFeed,
    staleTime: 60_000,
  });
}

Slug detail endpoint

Return 404 for missing content so app state can handle unavailable routes cleanly.

// backend/routes/mobile-post.ts
router.get("/mobile/posts/:slug", async (req, res, next) => {
  try {
    const result = await cms().getEntries("posts", {
      where: { slug: req.params.slug },
      limit: 1,
      state: "published",
    });
 
    const rows = Array.isArray(result) ? result : result.data;
    const post = rows[0];
    if (!post) return res.status(404).json({ error: "Not found" });
 
    return res.json({ item: post });
  } catch (error) {
    next(error);
  }
});
Sync

Use webhooks for cache and refresh flows

ElmapiCMS webhooks can notify your backend when content changes. From there you can purge edge caches, warm mobile endpoints, or trigger in-app refresh signals.

// backend/routes/elmapi-webhook.ts
import { createHmac, timingSafeEqual } from "node:crypto";
 
router.post("/webhooks/elmapicms", express.text({ type: "*/*" }), async (req, res) => {
  const rawBody = req.body ?? "";
  const signature = req.header("x-webhook-signature");
  const secret = process.env.ELMAPI_WEBHOOK_SECRET;
 
  if (secret) {
    if (!signature) return res.status(401).json({ error: "Missing signature" });
 
    const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
    const valid =
      signature.length === expected.length &&
      timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
 
    if (!valid) return res.status(401).json({ error: "Invalid signature" });
  }
 
  // Optional: purge edge cache or notify app clients via push/in-app refresh.
  return res.json({ ok: true });
});

Configure webhook events in Project Settings → Webhooksand review production queue requirements in the deployment webhooks docs.

Checklist

Before you ship

  • No privileged CMS tokens are embedded in React Native app code.
  • Backend endpoints fetch only state: "published" content for public clients.
  • List endpoints are paginated and only request fields used by the screen.
  • App caching strategy is defined (React Query stale times and retry behavior).
  • Webhook signature verification is enabled before cache invalidation logic.

Last updated: June 28, 2026