Stop 07 · TypeScript Across the StackTopics · 4/7

Sharing Types Between Frontend & Backend

Part of the Full Stack Developer Roadmap

In short

If the API and the React app each keep their own User type, one day only one of them changes. Put it in one shared place and both sides break together, loudly, at build time.

Defining your data models once (e.g. in a shared package or monorepo) so frontend and backend can't silently drift apart on what a `User` object actually looks like.

The mascot holds one big guest list notebook as relatives from both families read it.

Think of it like this

One guest list for both families

At a wedding, if the bride's side and the groom's side each keep a separate guest list, someone gets invited twice and someone is forgotten. Smart families keep one shared list both sides read from. Shared types are that single list.

Watch it happen

A monorepo with a shared package

my-app/
packages/shared
user.ts
apps/web
UserCard.tsx
apps/api
users.route.ts

1/5One repo holds the frontend, the backend and a shared package, linked with npm or pnpm workspaces.

The big ideas

What you need to know

A shared workspace package

Make packages/shared with its own package.json name like @myapp/shared, and depend on it from both apps.

Share schemas, not just types

Export a Zod schema and infer the type from it. The API validates input with it, the frontend validates responses.

Never share server secrets

The shared package goes into the browser bundle. Keep database code and secrets in the API app only.

import type for type-only

import type { User } is erased at build time, so no runtime code gets pulled in by accident.

See it in code

Walk through the code

ts
1// packages/shared/src/user.ts2import { z } from "zod";3export const UserSchema = z.object({ id: z.string(), name: z.string(), email: z.email() });4export type User = z.infer<typeof UserSchema>;5 6// apps/api/src/users.route.ts7import type { User } from "@myapp/shared";8app.get("/api/users/:id", async (req, res) => {9  const user: User = await getUser(req.params.id);10  res.json(user);11});12 13// apps/web/src/api.ts14import { UserSchema, type User } from "@myapp/shared";15export const fetchUser = async (id: string): Promise<User> =>16  UserSchema.parse(await (await fetch(`/api/users/${id}`)).json());

1/3The schema lives in the shared package. The User type is inferred from it.

Three files, one definition of User. Uses Zod 4, where z.email() is a top-level helper.

Save yourself the pain

Common mistakes

Oops

Copy-pasting types.ts into both the frontend and backend repos.

Do this instead

Use one shared package in a monorepo, or publish it as a private package both apps install.

Oops

Putting the Prisma client or database helpers in the shared package.

Do this instead

Share only types, schemas and pure helpers. Anything server-only must stay in the API app.

Your mission

Set up a tiny monorepo

~45 min

Quick check

Why export a Zod schema from the shared package instead of only a TypeScript type?

Go deeper

  1. 1Set up a shared `types` folder or package used by both your frontend and backend.
  2. 2Change a shared type and confirm both codebases immediately show the resulting type errors.
  3. 3Learn how monorepo tools (Turborepo, Nx) make this pattern practical at scale.

More topics at this stop

Stuck on this topic? Ask an Insider

Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.

Download
InsideEdge