Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Auth and the API Contract  ›  Capstone Project

One Error Shape, Checked at Every Layer

RESTful APIs 15 minValidation happens in the browser for fast feedback, on the server as the check that counts, and in database constraints as the last line, and the API reports every error in one consistent shape the client can display.

One Error Shape, Checked at Every Layer

A user types 0 into "water every ... days". Where should that be caught: in the form, on the server, or in the database? The answer is all three, each for a different reason. And when the server catches it, the error must travel back in a shape the client can show right next to the field that caused it.

The rule

Check in the browser for speed, on the server for truth, in the database as the last line.

  • Browser. <input type="number" min="1" required> and small JavaScript checks give instant feedback without a round trip. They are a courtesy, and anyone can bypass them.
  • Server. Validate every field of every request. This is the check that counts, because it is the only one users cannot skip.
  • Database. Constraints such as NOT NULL, UNIQUE and CHECK (water_every_days > 0) catch what slips past: a bug in your own server code, or a route someone forgot to validate.

So far, Sprout's errors have been a bare message such as { error: 'Plant not found' }. That is fine for one sentence, but it gives the client nowhere to put "this field is wrong". Grow it into one shape for the whole API:

{
  "error": {
    "code": "validation_failed",
    "message": "Some fields need fixing.",
    "fields": { "water_every_days": "Must be at least 1 day." }
  }
}
const days = Number(req.body.water_every_days);
if (!Number.isInteger(days) || days < 1) {
  return res.status(422).json({ error: {
    code: 'validation_failed',
    message: 'Some fields need fixing.',
    fields: { water_every_days: 'Must be at least 1 day.' },
  } });
}

Because every error looks the same, one helper on the client handles them all: message goes into a banner, and each entry in fields goes under its input.

Unexpected failures need care too. When something truly breaks, answer 500 with a short, generic message and write the details to the server's log. Stack traces and SQL in a response help attackers, not users. Some database errors are really user errors in disguise: a UNIQUE violation on email (PostgreSQL error code 23505) should become 409 with "That email is already registered".

The shortcut

Place each rule by asking "Who would break it?" An honest typo: the browser catches it first, the server again. A modified client or a hand-made request: the server. Your own bug, or a path nobody validated: the database constraint.

A "0 days" plant, three waysStep 1 of 4
  1. An honest typo

    The form's min="1" blocks the submit and shows a hint at once. No request is sent.

Drills

Answer fast — trust the rule.

quick drill

Which layer's check can Sprout never skip?

quick drill

Why keep a database CHECK when the server already validates?

quick drill

What should the body of a 500 response contain?

quick drill

How does one error shape across the whole API help the client?

quick drill

Sign-up hits the UNIQUE constraint on users.email. What should the API answer?

Remember

  • Check in the browser for speed, on the server for truth, in the database as the last line.
  • Shortcut: "Who would break it?" A typo, a hand-made request, or your own bug each meets a different layer.
  • Trap: leaking stack traces in 500s, and inventing a new error format for every route.

Practice — level up

0/1 solved

Write your answer, then compare it with a rubric and a model answer.

ExplainNames from 1 to 40 characters

Sprout decides that plant names must be 1 to 40 characters long. Explain how you would enforce this in each of the three layers, what the server sends back when the rule is broken, and how the client shows it.