Stop Writing REST APIs From Scratch in 2025

If you’ve been a backend or full-stack developer for any length of time, you know the ritual. A new feature requires a new API endpoint, and the boilerplate ceremony begins: define the route, write the controller, validate input, handle errors, and update the docs.

This process isn’t just tedious — it’s fragile. Every extra definition or cast is a chance for a silent bug: mismatched types, stale documentation, or forgotten validation. Developers have accepted this as the cost of reliability.

But in 2025, it’s time to challenge that assumption. Building APIs manually is an anti-pattern. The modern ecosystem offers something better — a schema-driven paradigm that replaces repetitive setup with declarative contracts.

This article deconstructs the old way, introduces the schema-driven model, and shows why writing REST APIs from scratch no longer makes sense.

A “classic” REST endpoint setup

Let’s illustrate the problem by building a simple POST /users endpoint the “classic” way, using Express and yup.

import * as yup from 'yup';

// Definition 1: TypeScript interface
interface CreateUserRequest {
  username: string;
  email: string;
  age: number;
}

// Definition 2: Validation schema
const createUserSchema = yup.object({
  username: yup.string().min(3).required(),
  email: yup.string().email().required(),
  age: yup.number().positive().integer().required(),
});

Immediately, we’ve defined the same structure twice — violating DRY and creating sync issues.

Now, the endpoint itself:

import express, { Request, Response, NextFunction } from 'express';
import * as yup from 'yup';

const app = express();
app.use(express.json());

const validate = (schema: yup.AnyObjectSchema) =>
  async (req: Request, res: Response, next: NextFunction) => {
    try {
      await schema.validate(req.body);
      next();
    } catch (err) {
      res.status(400).json({ type: 'validation_error', message: err.message });
    }
  };

app.post('/users', validate(createUserSchema), (req, res) => {
  const userData = req.body as CreateUserRequest;
  try {
    const newUser = { id: Date.now(), ...userData };
    res.status(201).json(newUser);
  } catch {
    res.status(500).json({ message: 'Internal server error' });
  }
});

We’ve repeated the same ceremony: duplicate schemas, manual validation middleware, explicit type casting, and try/catch clutter.
To make things worse, we’d still need to manually update our OpenAPI docs — a third source of truth bound to drift.

The schema-driven solution

The alternative is a declarative model: define your contract once and let your framework handle routing, validation, and documentation.

Let’s rebuild the same endpoint using tRPC with Zod as our single source of truth.

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

const t = initTRPC.create();

const createUserSchema = z.object({
  username: z.string().min(3),
  email: z.string().email(),
  age: z.number().positive().int(),
});

export const appRouter = t.router({
  createUser: t.procedure
    .input(createUserSchema)
    .mutation(({ input }) => {
      const newUser = { id: Date.now(), ...input };
      return newUser;
    }),
});

export type AppRouter = typeof appRouter;

Here’s what changed:

  • One schema, one truth. Types are inferred automatically from Zod.
  • No middleware. Validation is built in.
  • No type casting. Inputs and outputs are strongly typed.
  • No try/catch. Errors are handled gracefully by the framework.

The result: faster iteration, fewer bugs, and self-documenting code.

Frameworks embracing schema-driven APIs

This shift isn’t limited to tRPC — it’s a broader industry trend. Here’s how three other frameworks implement similar principles.

Hono: Web standards meet type safety

import { Hono } from 'hono';
import { z } from 'zod';
import { zValidator } from '@hono/zod-validator';

const app = new Hono();
const createUserSchema = z.object({
  username: z.string().min(3),
  email: z.string().email(),
  age: z.number().positive().int(),
});

app.post('/users', zValidator('json', createUserSchema), (c) => {
  const userData = c.req.valid('json');
  const newUser = { id: Date.now(), ...userData };
  return c.json(newUser, 201);
});

Hono modernizes Express-style syntax with built-in validation middleware — minimal setup, full type safety.

Fastify: Schema-driven performance

import Fastify from 'fastify';
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';

const fastify = Fastify();
const createUserSchema = z.object({
  username: z.string().min(3),
  email: z.string().email(),
  age: z.number().positive().int(),
});

type CreateUserRequest = z.infer;

fastify.post<{ Body: CreateUserRequest }>('/users', {
  schema: { body: zodToJsonSchema(createUserSchema) },
}, async (request, reply) => {
  const newUser = { id: Date.now(), ...request.body };
  reply.code(201).send(newUser);
});

Fastify uses schemas for both validation and performance optimization, turning type safety into runtime efficiency.

NestJS: Declarative via decorators

import { Controller, Post, Body } from '@nestjs/common';
import { IsString, IsEmail, IsInt, Min, MinLength } from 'class-validator';

export class CreateUserDto {
  @IsString()
  @MinLength(3)
  username: string;

  @IsEmail()
  email: string;

  @IsInt()
  @Min(1)
  age: number;
}

@Controller('users')
export class UsersController {
  @Post()
  create(@Body() userData: CreateUserDto) {
    return { id: Date.now(), ...userData };
  }
}

NestJS integrates validation and typing through class decorators — no manual wiring needed.

The payoff: faster, safer, and self-documenting

The schema-driven paradigm offers measurable improvements across the board:

Aspect Classic REST (Express + yup) Schema-Driven (tRPC + Zod)
Development velocity Slow and verbose: multiple schemas, middleware, and manual error handling. Rapid and concise: one schema defines the entire contract; plumbing handled by framework.
Safety and reliability Brittle: manual type casting and sync issues between layers. End-to-end typesafe: schema shared across server and client with compile-time validation.
Documentation Manual and stale: separate OpenAPI spec that drifts over time. Automatic and current: tools like trpc-openapi generate live documentation from code.

Conclusion

Building APIs manually is a relic of the past. The schema-driven approach replaces repetitive glue code with declarative contracts, letting frameworks handle the boilerplate.

It’s not about writing less code — it’s about writing better code. A single schema becomes your validation layer, type system, and documentation. Your APIs are faster to build, safer to evolve, and easier to maintain.

The message is simple: stop writing REST APIs from scratch. The frameworks of 2025 already know how to do it for you.

The post Stop Writing REST APIs From Scratch in 2025 appeared first on LogRocket Blog.

 

This post first appeared on Read More