Skip to main content
Course/Module 10/Topic 2 of 4Advanced

Type-Safe Express API

Build a type-safe Express API — typed request handlers, middleware, validation with Zod, and structured error handling.

55 minBy Priygop TeamLast updated: Feb 2026

Typed API Routes

  • Typed Handler: type TypedHandler<P, ResBody, ReqBody> = (req: Request<P, ResBody, ReqBody>, res: Response<ApiResponse<ResBody>>) => Promise<void> — generic handler type enforcing request/response shapes
  • Controller Pattern: const getUser: TypedHandler<{id: string}, User, never> = async (req, res) => { const user = await userService.findById(Number(req.params.id)); if (!user) { res.status(404).json({ success: false, error: 'User not found' }); return; } res.json({ success: true, data: user }); }
  • Zod Validation: const CreateUserSchema = z.object({ email: z.string().email(), name: z.string().min(2).max(100), password: z.string().min(8) }); type CreateUserInput = z.infer<typeof CreateUserSchema> — schema defines both runtime validation and TypeScript type
  • Validation Middleware: function validate<T>(schema: z.Schema<T>) { return (req: Request, res: Response, next: NextFunction) => { const result = schema.safeParse(req.body); if (!result.success) { res.status(400).json({ success: false, error: result.error.message }); return; } req.body = result.data; next(); }; }
  • Error Classes: class AppError extends Error { constructor(public message: string, public statusCode: number, public code: string) { super(message); } } class NotFoundError extends AppError { constructor(resource: string) { super(`${resource} not found`, 404, 'NOT_FOUND'); } } — typed error hierarchy
  • Error Middleware: app.use((err: Error, req: Request, res: Response, next: NextFunction) => { if (err instanceof AppError) { res.status(err.statusCode).json({ success: false, error: err.message }); } else { res.status(500).json({ success: false, error: 'Internal server error' }); } }) — centralized error handling

Service Layer & Database

  • Service Interface: interface UserService { findAll(params: PaginationParams): Promise<PaginatedResult<User>>; findById(id: number): Promise<User | null>; create(data: CreateUserDTO): Promise<User>; update(id: number, data: UpdateUserDTO): Promise<User>; delete(id: number): Promise<void>; } — contract for business logic
  • Generic Repository: interface Repository<T, CreateDTO, UpdateDTO> { findAll(): Promise<T[]>; findById(id: number): Promise<T | null>; create(data: CreateDTO): Promise<T>; update(id: number, data: UpdateDTO): Promise<T>; delete(id: number): Promise<void>; } — reusable for any entity
  • Type-Safe Query Builder: const users = await db.select().from(usersTable).where(eq(usersTable.email, email)).limit(1); — Drizzle ORM provides full TypeScript inference from schema. Result type matches table definition
  • JWT Typing: interface JwtPayload { userId: number; role: 'admin' | 'user'; iat: number; exp: number; } const payload = jwt.verify(token, secret) as JwtPayload — typed payload after verification
  • Auth Middleware: declare global { namespace Express { interface Request { user?: JwtPayload; } } } — extend Express Request type to include authenticated user. Middleware sets req.user after JWT verification
  • Environment Validation: const envSchema = z.object({ DATABASE_URL: z.string().url(), JWT_SECRET: z.string().min(32), PORT: z.string().regex(/^\d+$/) }); const env = envSchema.parse(process.env) — validate environment variables at startup, crash fast if misconfigured
Chat on WhatsApp
Priygop - Leading Professional Development Platform | Expert Courses & Interview Prep