Skip to content
PAPRADEEPADHIKARI
React NativeExpoNext.jsArchitecture

One Backend, Two Clients: Architecture for a Next.js Web App and an Expo Mobile App

How to share an API, validation schemas and data-fetching patterns between a Next.js web app and an Expo / React Native app without duplicating business rules.

Pradeep Adhikari
· 5 min read

When a product needs a website and a phone app, the tempting shortcut is to build two separate applications that each talk to the database in their own way. It works until the first rule changes and has to be changed twice, subtly differently. A sturdier approach treats the backend as the single place where rules live and builds both clients as thin consumers of it.

Put the rules behind an API

The shape that scales is a standalone API service — for example Node.js with Express — that owns authentication, authorisation, validation and the database. The web app and the mobile app are both clients of that API.

 ┌────────────┐   ┌────────────┐   ┌─────────────┐
 │ Next.js    │   │ Expo / RN  │   │ Admin app   │
 │ web app    │   │ mobile app │   │ (Next.js)   │
 └─────┬──────┘   └─────┬──────┘   └──────┬──────┘
       └────────────────┼─────────────────┘
                        ▼
                ┌───────────────┐
                │  REST API     │  auth · rules · validation
                └───────┬───────┘
                        ▼
                ┌───────────────┐
                │ MongoDB       │
                └───────────────┘

Next.js can also host API routes, and for a site with a single client that is often the right call. Once a second client with a different release cycle appears — an app store build cannot be rolled back in minutes — a dedicated API gives you a stable, versionable contract. The cost is an extra service to deploy and secure.

Authentication that both clients can use

Browsers and native apps store credentials differently, and a design that favours one makes the other awkward.

  • Web: keep the refresh token in an HttpOnly, Secure, SameSite cookie so scripts cannot read it. Keep short-lived access tokens in memory.
  • Mobile: there are no cookies to rely on. Store tokens in the platform keychain through expo-secure-store, not in plain async storage.
  • Both: short-lived access tokens, rotating refresh tokens, and the ability to revoke a session server-side.

The API can accept an access token in an Authorization: Bearer header from both clients and keep cookie handling as a web-specific layer on top.

Share contracts, not UI

Web and native UIs are different. Share what is genuinely the same: the types and validation of the data crossing the API. A schema library like Zod gives you both runtime validation and TypeScript types from one definition:

// shared/schemas.ts
import { z } from "zod";

export const submitAnswerSchema = z.object({
  quizId: z.string(),
  answers: z.record(z.string(), z.string()),
});

export type SubmitAnswerInput = z.infer<typeof submitAnswerSchema>;

The server validates requests with it. Clients use the inferred type to build requests and, in forms, can reuse the schema for instant feedback. Whether the shared code lives in a monorepo package or is copied by a build step, the principle is the same: one definition of the contract.

A shared data-fetching pattern

TanStack Query works on both React (web) and React Native, which means the same mental model, hooks and caching behaviour on both platforms. A typical query hook looks identical in either app apart from the HTTP client's base configuration:

export function useChapterProgress(subjectId: string) {
  return useQuery({
    queryKey: ["progress", subjectId],
    queryFn: () => api.get(`/subjects/${subjectId}/progress`).then((r) => r.data),
    staleTime: 60_000,
  });
}

Mutations invalidate the relevant query keys so that completing a lesson on the phone updates the dashboard the next time the web app focuses. Because the server is the source of truth, the two clients converge without special sync code.

Where the web app uses the server directly

A Next.js web app does not have to behave like a mobile client. Public, indexable pages — a course catalogue or a lesson overview — are best rendered on the server and can call the same data layer or API during render, which gives crawlers real HTML (see Server vs Client Components in Next.js). Interactive, signed-in areas then use client-side queries. The mobile app has no such split; everything is a client.

Differences you must design for

  • Offline and flaky networks. Phones lose connectivity. Cache reads, queue or retry writes that are safe to repeat, and make progress updates idempotent so retries do not duplicate state.
  • Media. Video is usually delivered as HLS streams, which both a web player and the native player can consume. Large uploads from an admin tool benefit from a resumable protocol so a dropped connection does not restart a multi-gigabyte upload.
  • Payments. App stores have their own purchase rules for digital goods on mobile, which differ from web payments. Keep the rule — "this user now has access" — in the backend, and let each platform's purchase flow feed into it. The entitlement approach in Cart, Checkout and Access Entitlements is designed for exactly this.
  • Releases. A web deploy reaches everyone immediately; an app update does not. Version the API, keep changes additive, and give the app a way to learn that it is too old to work and must update.
  • Push notifications. Store device tokens per user and device on the server, and handle token rotation and invalid tokens when sending.

Real-time updates

If features need live updates — notifications, live counts, collaborative views — a WebSocket layer such as Socket.IO works from both the browser and React Native. When running more than one server instance, add a shared adapter (Redis is the usual choice) so events reach clients connected to different instances.

Observability

With several clients on one backend, errors need context. Add an error-tracking tool to each client and the API, tag events with the app and version, and log request IDs end to end. When something fails on a phone, you want to find the matching server log line quickly.

A few rules of thumb

  • Rules live in the API, never only in a client.
  • Authorisation is checked on the server for every protected request.
  • Share schemas and types; do not force shared UI.
  • Make writes idempotent so retries are safe.
  • Version the API and prefer additive changes.
  • Assume the mobile client is out of date.

This is the architecture behind multi-client products such as E2A Learning, which pairs a Next.js web app, an admin app and an Expo mobile app with a shared backend. For the data side of the same system, see Designing an LMS Data Model.