Skip to content
By Yash KapureFrontend10 min readMay 23, 2026

API Contracts Between Frontend and Backend

The frontend should never discover a breaking API change in production. Shared contracts make mismatches visible in CI, not at 3 AM.

Article focus

Zero runtime surprises

When contracts are shared and enforced

Key takeaways

  • Shared types (Zod, tRPC, or a types package) turn API mismatches into compile errors.
  • Contract testing catches breaking changes before they reach production.
  • Even without a monorepo, you can share types via a published package or code generation.

Why do API mismatches keep happening?

Because the frontend and backend each have their own definition of the data shape, and nothing enforces they stay in sync.

The frontend expects user.profilePicture. The backend sends profile_picture. The page renders a broken image icon. No error, no warning, no stack trace. Just a missing pixel that nobody notices until a customer complains.

This is not a skill problem. It is a communication problem. The frontend team and the backend team (or the same developer wearing both hats) each maintain their own mental model of the data. Those models drift over time because nothing holds them together.

The fix is a single source of truth for the shape of data that crosses the network boundary. Define it once. Reference it from both sides. Let the type checker enforce the agreement. This is one of the five pillars in the Frontend Architecture framework.

Live Schema Validation Demo

Toggle between valid and invalid payloads, or edit the JSON directly. The validation runs in real time.

Validation Result
id (uuid)
Valid
email
Valid
displayName
Valid
role
Valid
All validations passed - contract is satisfied

Edit the JSON or click presets. The validation runs in real time, just like a Zod schema.

Option 1: Zod schemas as the contract

Define schemas in a shared package. Frontend and backend both import them. Validation and types from one source.

Zod is my default choice for API contracts because it serves two purposes simultaneously: it validates data at runtime and infers TypeScript types at compile time. One definition, two guarantees.

The key is that the schema lives in a place both sides can import. In a monorepo, that is a shared package. In a multi-repo setup, it can be a published npm package or generated from an OpenAPI spec.

typescript

// shared/schemas/user.ts - the single source of truth
export const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  displayName: z.string().min(1).max(100),
  avatarUrl: z.string().url().optional(),
  role: z.enum(['admin', 'member', 'viewer']),
  createdAt: z.string().datetime(),
});

export type User = z.infer<typeof UserSchema>;

// Backend: validate the response before sending
app.get('/api/users/:id', (req, res) => {
  const user = await db.findUser(req.params.id);
  res.json(UserSchema.parse(user)); // throws if shape is wrong
});

// Frontend: validate the response after receiving
const { data, error } = await client
  .from('/api/users/123')
  .get();

const user = UserSchema.parse(data); // catches drift immediately

Option 2: tRPC - no contract, just functions

tRPC eliminates the API contract entirely by making backend procedures callable as typed functions from the frontend.

tRPC takes a different approach. Instead of defining a contract between two sides, it removes the boundary. Backend procedures become directly callable from the frontend with full type safety. No REST endpoints, no manual parsing, no duplicate type definitions.

tRPC shines in full-stack TypeScript projects (especially Next.js or Express) where the frontend and backend are in the same repository. It eliminates an entire category of bugs - the "I renamed a field on the backend but forgot to update the frontend" category.

The trade-off is that it tightly couples your frontend to your backend framework. If your backend is not TypeScript (Python, Go, Rust), tRPC is not an option. In that case, Zod schemas or OpenAPI generation are the better path.

typescript

// Backend: define the procedure
export const userRouter = t.router({
  getById: t.procedure
    .input(z.string().uuid())
    .query(async ({ input }) => {
      return db.findUser(input);
    }),
});

// Frontend: call it like a function - fully typed
const user = await trpc.user.getById.query('123');
// user is typed as the return type of the backend procedure

Option 3: OpenAPI + code generation

Define the API in an OpenAPI spec. Generate client code for the frontend. The spec is the contract.

When the backend is not TypeScript, OpenAPI is the lingua franca. Define your API in an OpenAPI 3.x spec. Use a tool like openapi-typescript or openapi-generator to generate typed frontend clients.

The generation step is critical. If you write the types by hand based on the OpenAPI spec, you have already introduced drift potential. Generate them automatically in CI and check the diff.

yaml

# openapi.yaml - the contract
paths:
  /users/{id}:
    get:
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
components:
  schemas:
    User:
      type: object
      properties:
        id: { type: string, format: uuid }
        displayName: { type: string }
        avatarUrl: { type: string, format: uri }
        role: { type: string, enum: [admin, member, viewer] }

Contract testing - the safety net

Even with shared types, contract tests verify that the actual API response matches the expected schema in CI.

Shared types catch most mismatches, but they cannot catch everything. What if the backend changes the response shape but forgets to update the shared schema? What if a new field is added that breaks a strict validation?

Contract testing solves this. It is a small suite of tests that hit the real API (or a recorded response) and validate the response against the expected schema.

Run these in CI after deployment but before switching traffic. Or run them against a staging environment. The goal is to catch mismatches before they reach users.

typescript

// contract-tests/users.test.ts
import { UserSchema } from '@shared/schemas/user';

describe('GET /api/users/:id', () => {
  it('returns a valid user shape', async () => {
    const response = await fetch('/api/users/123');
    const data = await response.json();

    // This throws if the API response
    // does not match the expected shape
    expect(() => UserSchema.parse(data)).not.toThrow();
  });

  it('returns the correct role values', async () => {
    const response = await fetch('/api/users/123');
    const data = await response.json();

    expect(data.role).toMatch(/^(admin|member|viewer)$/);
  });
});

A tiered approach for existing projects

Start with runtime validation on the frontend, add shared types for the most critical endpoints, then add contract tests in CI.

If you are working in a codebase with no contracts today, do not try to fix everything at once. Use a tiered approach:

Each tier adds protection without requiring a full rewrite. Start with the endpoints that change most often or cause the most damage when they break.

  • Tier 1 - Add Zod validation on the frontend for API responses. At minimum, parse the response and log or surface errors. This protects users from silent failures.
  • Tier 2 - Extract schemas into a shared location (even just a file that both sides copy from, though shared is better). Focus on the most frequently changing endpoints.
  • Tier 3 - Add contract tests in CI for critical paths (auth, billing, data export). These are high-value, low-maintenance.
  • Tier 4 - Move toward a monorepo or shared package so the types are always in sync. Generate clients from OpenAPI if the backend is not TypeScript.

Try It: Tiered Approach Explorer

Click each tier to see details, mark progress, and watch protection build up as you go.

Effort: Low
Protection: 30%

Add Zod parsing on the frontend for API responses. Surface errors instead of silent failures.

  • Parse API responses with Zod schemas
  • Surface errors instead of silent failures
  • Takes ~10 minutes per endpoint
TypeScript
const parsed = UserSchema.parse(data);
// throws if shape is wrong - loud failure

Cumulative Protection by Tier

Each tier adds measurable protection. Even Tier 1 alone catches most silent failures.

0 of 4 tiers implemented

Need help implementing this?

I build these systems for a living - let's work on yours.

Frontend Architecture & System Design

Structure so teams can ship. Clear boundaries, state strategy, and contracts. New features land cleanly; refactors stay low-risk.

TypeScriptComponent designState managementAPI contracts+4
Discuss this service

Full Stack Development

End-to-end apps: MERN, React+Node, or MEVN. Auth, role-based access, and real-time where it matters. Designed to scale without rewrites.

MERNMEVNNode.jsExpressReact+5
Discuss this service

Frontend Development

Production UIs in React, Next.js, or Vue- third-party and payment integrations included. Built for real traffic and maintained in production.

ReactNext.jsTypeScriptTailwind CSSshadcn/ui+5
Discuss this service
$ git checkout --your-opinion

Your turn

  • >Did this help you ship something?
  • >Which part clicked the most for you?
  • >Applying this at work? Share your experience.

Discussion

Leave a comment

Thoughts, questions, corrections - all welcome.

Sign in with GitHub to comment - free & takes 10 seconds

Recommended blogs

Continue reading

View all blogs
Laptop on a clean desk in a modern room
Frontend
9 min readMarch 5, 2026

Shipping React UI Fast Without Making a Mess

The way I structure React and Next.js UI so the team ships fast because the system is obvious, not because we skipped every guardrail.

Photo by Zak Chapman on Pexels

Read article
Abstract architecture blueprint with glass and steel structure
Frontend
12 min readMay 16, 2026

Frontend Architecture: The Mental Model That Keeps Complex Apps Maintainable

Five interconnected pillars - components, state, contracts, testing, tooling - that keep a frontend codebase healthy as the team and features grow.

Photo by ThisIsEngineering on Pexels

Read article

Subscribe for new posts, or read how referrals and sponsored placements are handled on this site.