VOOZH about

URL: https://blog.logrocket.com/stop-writing-rest-apis-from-scratch/

โ‡ฑ Stop writing REST APIs from scratch in 2025 - LogRocket Blog


2025-10-14
907
#typescript
Ikeh Akinyemi
208331
116
๐Ÿ‘ Image

See how LogRocket's Galileo AI surfaces the most severe issues for you

No signup required

Check it out

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.

๐Ÿ‘ Image

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.

๐Ÿš€ Sign up for The Replay newsletter

The Replay is a weekly newsletter for dev and engineering leaders.

Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.

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.


Over 200k developers use LogRocket to create better digital experiences

๐Ÿ‘ Image
Learn more โ†’

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.

LogRocket understands everything users do in your web and mobile apps.

๐Ÿ‘ LogRocket Dashboard Free Trial Banner

LogRocket lets you replay user sessions, eliminating guesswork by showing exactly what users experienced. It captures console logs, errors, network requests, and pixel-perfect DOM recordings โ€” compatible with all frameworks, and with plugins to log additional context from Redux, Vuex, and @ngrx/store.

With Galileo AI, you can instantly identify and explain user struggles with automated monitoring of your entire product experience.

Modernize how you understand your web and mobile apps โ€” start monitoring for free.

๐Ÿ‘ Image
๐Ÿ‘ Image
๐Ÿ‘ Image

Stop guessing about your digital experience with LogRocket

Get started for free

Recent posts:

Stop hardcoding LLM SDKs: Dynamic LLM routing with OpenRouter and Next.js

Build dynamic LLM routing in Next.js with OpenRouter, TanStack AI, task classification, model fallbacks, and cost-aware routing.

๐Ÿ‘ Image
Chizaram Ken
Jun 16, 2026 โ‹… 13 min read

What is TSRX?: What JSX would look like if it were designed today

TSRX adds first-class control flow, conditional hooks, and scoped styles to React via a TypeScript compiler extension โ€” no new framework required.

๐Ÿ‘ Image
Ikeh Akinyemi
Jun 12, 2026 โ‹… 6 min read

How to add authentication to a React Native app with Better Auth

Learn how to build a full React Native auth system using Better Auth and Expo โ€” with email/password login, Google OAuth, session persistence, and protected routes.

๐Ÿ‘ Image
Chinwike Maduabuchi
Jun 9, 2026 โ‹… 13 min read

AI dev tool power rankings & comparison [June 2026]

Compare the top AI development tools and models of June 2026. View updated rankings, feature breakdowns, and find the best fit for you.

๐Ÿ‘ Image
Chizaram Ken
Jun 8, 2026 โ‹… 11 min read
View all posts

Hey there, want to help make our blog better?

Join LogRocketโ€™s Content Advisory Board. Youโ€™ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.

Sign up now