Skip to main content

Building APIs with TypeScript: Type-Safe End to End

·APIScout Team
typescripttype-safe apitrpczodapi development

Building APIs with TypeScript: Type-Safe End to End

Type safety across the entire stack — from database schema to API response to frontend component — eliminates an entire class of bugs. No more "undefined is not a function" from a field that was renamed on the backend. TypeScript makes this possible when the right tools connect each layer.

The Type-Safe Stack

Database Schema (Prisma/Drizzle)
    → Type-safe ORM queries
API Layer (tRPC/Hono/Express + Zod)
    → Type-safe request validation + response types
Client (React Query + generated types)
    → Type-safe API calls with autocomplete

Layer 1: Database → TypeScript

Prisma

Prisma generates TypeScript types from your database schema:

model User {
  id    String @id @default(uuid())
  name  String
  email String @unique
}

Generates User type and type-safe query methods. prisma.user.findUnique({ where: { id } }) returns User | null with full autocomplete.

Drizzle ORM

Drizzle defines schemas in TypeScript directly:

const users = pgTable('users', {
  id: uuid('id').primaryKey().defaultRandom(),
  name: text('name').notNull(),
  email: text('email').notNull().unique(),
});

Types are inferred from the schema definition. Queries are type-safe with SQL-like syntax.

Layer 2: API Validation (Zod)

Zod validates runtime data and infers TypeScript types:

const CreateUserSchema = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email(),
  age: z.number().int().min(13).max(120).optional(),
});

type CreateUser = z.infer<typeof CreateUserSchema>;
// { name: string; email: string; age?: number }

One schema serves as: runtime validation, TypeScript type, and API documentation.

Layer 3: Type-Safe API

tRPC — Zero-API-Layer Approach

// Server
const appRouter = router({
  user: router({
    getById: publicProcedure
      .input(z.object({ id: z.string().uuid() }))
      .query(({ input }) => {
        return prisma.user.findUnique({ where: { id: input.id } });
      }),
    create: publicProcedure
      .input(CreateUserSchema)
      .mutation(({ input }) => {
        return prisma.user.create({ data: input });
      }),
  }),
});

// Client — full type inference, zero code generation
const user = await trpc.user.getById.query({ id: "abc" });
// user is typed as User | null

Hono + Zod — REST with Types

const app = new Hono();

const route = app.post('/users',
  zValidator('json', CreateUserSchema),
  async (c) => {
    const data = c.req.valid('json'); // Typed as CreateUser
    const user = await prisma.user.create({ data });
    return c.json(user);
  }
);

// Generate OpenAPI spec from routes
// Generate client types from OpenAPI

Express + Zod — Traditional REST

Use Zod middleware to validate request bodies, query parameters, and path parameters. Export Zod schemas to generate OpenAPI specs.

Layer 4: Client Types

tRPC Client (automatic)

Types flow from server to client automatically. No code generation needed.

OpenAPI → TypeScript (generated)

For REST APIs, generate TypeScript types from your OpenAPI spec:

npx openapi-typescript api.yaml -o ./types/api.ts

Then use with a typed fetch wrapper or generated client.

React Query + Types

const { data } = useQuery({
  queryKey: ['user', id],
  queryFn: () => trpc.user.getById.query({ id }),
});
// data is typed as User | null | undefined

The Full Pipeline

LayerToolTypes
DatabasePrisma / DrizzleGenerated from schema
ValidationZodInferred from validators
APItRPC / HonoInferred from procedures/routes
ClienttRPC client / openapi-typescriptAutomatic / generated
UIReact + TypeScriptFlows from API types

Benefits

  1. Rename a field on the server → TypeScript errors show everywhere it's used on the client
  2. Add a required field → Compilation fails until all callers provide it
  3. Change a type → No runtime "undefined" errors in production
  4. Autocomplete → Developers explore the API from their editor

When Not to Use tRPC

  • Public APIs — External developers need REST/GraphQL, not tRPC
  • Multi-language backends — tRPC is TypeScript-only
  • Existing REST API — Migration cost may not justify benefits
  • Large distributed teams — API contracts may need to be more explicit

For public APIs, use Zod + OpenAPI generation to get type safety internally while exposing a standard REST interface externally.


Building type-safe APIs? Explore TypeScript API tools and frameworks on APIScout — comparisons, guides, and developer resources.

Comments