Building APIs with TypeScript: Type-Safe End to End
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
| Layer | Tool | Types |
|---|---|---|
| Database | Prisma / Drizzle | Generated from schema |
| Validation | Zod | Inferred from validators |
| API | tRPC / Hono | Inferred from procedures/routes |
| Client | tRPC client / openapi-typescript | Automatic / generated |
| UI | React + TypeScript | Flows from API types |
Benefits
- Rename a field on the server → TypeScript errors show everywhere it's used on the client
- Add a required field → Compilation fails until all callers provide it
- Change a type → No runtime "undefined" errors in production
- 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.