Skip to content
matthewcm.dev

    ↑ ↓ move · ↵ open · tab scopeopen graph →

    API Modelling

    Put a layer between external APIs and your UI that parses raw responses into your own domain models, so a backend change touches one adapter instead of forty components.

    Evergreentended planted 2 min read
    On this page

    When a UI consumes an API directly, every component ends up knowing the backend’s field names, quirks and nesting. Then the backend renames cust_name to customerName, and you’re editing forty files. API modelling is the fix: define what you expect the API to deliver, as your own model, and translate at one boundary.

    The idea

    • Define the shape you need. Fields and types are explicit, so consumers don’t have to guess what the API returns.
    • Adapt at the edge. One function per resource turns the raw payload into your domain model. If the API changes, only the adapter changes.
    • Get tooling for free. Typed models give autocomplete and compile-time checks everywhere they’re used.

    This is the adapter pattern, sometimes called an anti-corruption layer, plus the dependency inversion from SOLID React: components depend on your model, not on someone else’s API.

    Validate, don’t just type

    TypeScript types vanish at runtime, so a type alone won’t save you from a payload that changed shape. A schema library parses and types in one step, and fails loudly at the boundary instead of quietly deep in a component:

    import { z } from "zod";
    
    const ApiCustomer = z.object({
      cust_name: z.string(),
      email_addr: z.string().email(),
      addr: z.object({ street: z.string(), city: z.string(), zip: z.string() }),
    });
    
    export type Customer = { name: string; email: string; address: { street: string; city: string; zip: string } };
    
    export const toCustomer = (raw: unknown): Customer => {
      const c = ApiCustomer.parse(raw); // throws on contract drift
      return { name: c.cust_name, email: c.email_addr, address: c.addr };
    };
    • Generate types from contracts when a spec exists (OpenAPI, GraphQL codegen), and keep a hand-written domain model for UI concerns on top.
    • Separate server state from client state. Data fetched from services is a cache; let a data library manage staleness and refetching, and keep only real UI state in stores.
    • A backend-for-frontend can do this modelling once, server-side, when one screen fans out to many services.

    Testing gets easier

    With a model layer, components test against plain domain objects, and fixture builders make test data readable. Mock at the network so the adapter runs too (MSW (Mock Service Worker)).

    In practice

    On an enterprise React app backed by many microservices, an explicit model layer meant that backend changes landed as small, contained adapter edits. Later, when we put an API gateway in front of everything, the switch was a small change for the same reason: the UI only knew about one layer. That story is in Rolling Out an API Gateway. Feature-based structure keeps these adapters next to the features that use them (Bulletproof React).

    • #api-design
    • #typescript
    • #zod
    • #architecture
    • #anti-corruption-layer

    ↪ Links to (4)