Stop 05 · Backend with Node.js & ExpressProjects · 2/2

CRUD API with Validation

Part of the Full Stack Developer Roadmap

In short

An API that accepts anything is an API that breaks. This project adds proper validation and error handling, so bad data is rejected at the door with a clear reason your React forms can show.

A more complete API with proper request validation and error handling — the direct backend counterpart to the React forms you'll build in Phase 9.

The mascot weighs a parcel on a scale and inspects its tag with a magnifying glass.

Think of it like this

The courier booking counter

The courier counter weighs your parcel, checks the PIN code and refuses anything over the limit before it enters the system. A wrong address gets fixed at the counter, not discovered three cities later. Validation is that counter for your API.

Watch it happen

Bad note, good note

React form
Express
Zod schema
Database

POST {title:""}

1/7The user submits a note with an empty title.

The big ideas

What you need to know

Validate at the edge

Check req.body, req.params and req.query as soon as the request arrives, before any database call.

Parse, don't just check

safeParse returns cleaned data: trimmed strings, defaults filled in, unknown fields dropped. Save that, not req.body.

PATCH uses a partial schema

Updates send only changed fields. Schema.partial() makes every field optional but still type-checks what arrives.

Field errors help the UI

Return { errors: { title: ["Title is required"] } } so each message can sit under its own input.

See it in code

Walk through the code

js
1import { z } from "zod";2 3const NoteInput = z.object({4  title: z.string().trim().min(1, "Title is required").max(120),5  body: z.string().max(5000).default(""),6  tags: z.array(z.string()).max(5).optional(),7});8 9app.post("/api/notes", async (req, res) => {10  const parsed = NoteInput.safeParse(req.body);11  if (!parsed.success) {12    return res.status(400).json({ errors: z.flattenError(parsed.error).fieldErrors });13  }14  const note = await db.note.create({ data: parsed.data });15  res.status(201).json(note);16});

1/3The schema describes a valid note: a trimmed title up to 120 chars, optional body and up to five tags.

This uses Zod 4. The same NoteInput schema can be imported by your React form too.

Save yourself the pain

Common mistakes

Oops

Passing req.body straight into db.create(), so a user can send isAdmin: true.

Do this instead

Save only the parsed data. Zod objects drop unknown keys by default, so extra fields never reach the DB.

Oops

Returning 500 for bad input because the database threw an error.

Do this instead

Catch bad input with validation and return 400. A 500 should mean your code broke, not the user's.

Your mission

First milestone: validated notes

~45 min

Quick check

A client sends { title: "Hi", role: "admin" } to your Zod-validated route. What ends up in parsed.data?

Go deeper

  1. 1Add full validation to every write endpoint (create/update).
  2. 2Return field-specific validation errors in a consistent shape.
  3. 3Write a few tests confirming invalid requests are correctly rejected.

More projects at this stop

Stuck on this topic? Ask an Insider

Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.

Download
InsideEdge