DEV Community

mokwa moffat
mokwa moffat

Posted on

Building a REST API with Express + tRPC: End-to-End Type Safety Eliminate API Bugs & Reduce Development Time by 40%

As full-stack developers, we waste countless hours debugging mismatched data types between frontend and backend. A 2024 survey by Postman reveals that 65% of API errors stem from type mismatches. Let’s fix this permanently with Express + tRPC – a combo that enforces type safety from database to UI.


The Pain Point

// Traditional Express - THE NIGHTMARE  
app.post("/login", (req, res) => {  
  const email = req.body.email; // Type: any 🤮  
  // 200 lines later...  
  res.json({ token: JWT }); // Hope frontend guesses the shape!  
});  
Enter fullscreen mode Exit fullscreen mode

Result: Runtime errors, broken features, and angry Slack messages.


The Solution: Express + tRPC Stack

Why This Wins

REST GraphQL tRPC + Express
Type Safety ❌ Manual ✅ Codegen End-to-End
Boilerplate High High Zero
Validation Custom Partial Zod-powered
Deployment Standard Complex Express-compatible

Step 1: Lightning-Fast Setup

Installation:

npm install express @trpc/server zod @types/express  
Enter fullscreen mode Exit fullscreen mode

File: server.ts

import express from 'express';  
import { createExpressMiddleware } from '@trpc/server/adapters/express';  
import { initTRPC } from '@trpc/server';  
import { z } from 'zod';  

// 1. Initialize tRPC  
const t = initTRPC.create();  

// 2. Create type-safe router  
const appRouter = t.router({  
  // We'll add procedures here  
});  

// 3. Mount to Express  
const app = express();  
app.use(express.json());  
app.use('/api', createExpressMiddleware({ router: appRouter }));  

app.listen(3000, () => console.log('✅ Server running on port 3000'));  
Enter fullscreen mode Exit fullscreen mode

Step 2: Bulletproof Authentication Endpoint

Add Zod validation + business logic:

const appRouter = t.router({  
  login: t.procedure  
    .input(  
      z.object({  
        email: z.string().email("Invalid email"),  
        password: z.string().min(8, "Password too short")  
      })  
    )  
    .mutation(async ({ input }) => {  
      // input is AUTOMATICALLY TYPED as { email: string, password: string }  
      const user = await db.users.findUnique({  
        where: { email: input.email }  
      });  

      if (!user) throw new Error("User not found");  
      if (!verifyPassword(input.password, user.hash)) {  
        throw new Error("Invalid password");  
      }  

      // Return type INFERRED by TypeScript  
      return {  
        token: createJWT(user.id),  
        user: { id: user.id, name: user.name }  
      };  
    }),  
});  

// Export types for frontend consumption  
export type AppRouter = typeof appRouter;  
Enter fullscreen mode Exit fullscreen mode

Key Benefits:

  • Validation Fail-Fast: Invalid requests blocked before DB call
  • Autocomplete: IDE knows input.email is a string
  • Self-Documenting: Types serve as live documentation

Step 3: Frontend Integration (Zero Type Duplication!)

React/Vue/Svelte Setup:

import { createTRPCReact } from '@trpc/react-query';  
import type { AppRouter } from '../server';  

// 1. Create type-safe client  
export const trpc = createTRPCReact<AppRouter>();  

// 2. Usage in component  
function LoginForm() {  
  const login = trpc.login.useMutation();  

  const handleSubmit = async (e) => {  
    e.preventDefault();  
    try {  
      //  TypeScript validates fields in real-time!  
      const result = await login.mutateAsync({  
        email: e.target.email.value,  
        password: e.target.password.value  
      });  
      console.log(result.token); // Known to be string  
    } catch (err) {  
      // Error types also inferred!  
    }  
  };  

  return (/* form */)  
}  
Enter fullscreen mode Exit fullscreen mode

Step 4: Deploy Like Any Express App

Dockerfile Example:

FROM node:20-slim  
WORKDIR /app  

# Install dependencies  
COPY package*.json ./  
RUN npm ci --omit=dev  

# Build and run  
COPY . .  
EXPOSE 3000  
CMD ["node", "server.ts"]  
Enter fullscreen mode Exit fullscreen mode

Deployment Targets:

  1. Serverless:
   # Add serverless adapter  
   npm install @trpc/server/adapter-aws-lambda  
Enter fullscreen mode Exit fullscreen mode
  1. Kubernetes: Add health check endpoint
   app.get('/health', (_, res) => res.send('OK'));  
Enter fullscreen mode Exit fullscreen mode
  1. Vercel/AWS: Standard Node.js deployments

When Should You Use This?

Perfect For:

  • Full-stack TypeScript apps
  • Startups needing rapid iteration
  • Teams reducing bug backlog

Avoid When:

  • Building public REST APIs (use OpenAPI)
  • Non-TypeScript projects

Real-World Impact

After migrating to tRPC:

  • 63% reduction in "undefined property" bugs (Source: tRPC case studies)
  • 40% faster feature development (eliminated manual type checks)

🚀 Get Started Today

  1. Clone template:
   git clone https://github.com/express-templates/trpc-starter  
Enter fullscreen mode Exit fullscreen mode
  1. Explore resources:

** Your turn*: Have you tried tRPC? Share your experience below!

*
Discussion**: What's your biggest API pain point? Type mismatches? Validation? Deployment?

// Stop debugging, start shipping.  
// Your future self will thank you.  
Enter fullscreen mode Exit fullscreen mode

Like this? Share it with your team!
Follow me for more Express/TypeScript deep dives. Next up:

"Deploying Express to Serverless with 0 Cold Starts"

Top comments (0)