Lesson 4 of 5 · Full-Stack & Middleware Engineering
Hono + Zod Middleware
Middleware checks every request before your logic runs — like a bouncer checking IDs at the door.
This lesson is locked. You can read ahead, but finish earlier stops to earn progress.
Listen
Watch
Read
Start with the endpoint your activity actually uses
Middleware is a checkpoint in a request's journey: it can authenticate the caller, validate input or record diagnostics before the main operation runs. Hono is a lightweight framework with middleware chaining. Zod is a validation library that works with Hono, SvelteKit and other frameworks.
Your Level 5 activity uses SvelteKit, not a second Hono server. You do not need to install Hono to complete it. We will build the same checkpoint pattern in a SvelteKit request handler so the lesson and your project agree. Hono is an alternative implementation you can explore later, not an extra prerequisite.
The path is part of the contract
Create this file in your SvelteKit practice app:
src/routes/api/products/+server.ts
SvelteKit turns that path into /api/products. An exported POST handles POST requests; an exported GET would handle GET. A +page.svelte renders a page, while +server.ts returns a response. Creating an ordinary file called server.ts without the plus sign will not create this endpoint.
Trace the intended journey before writing code:
POST → parse JSON → validate fields → insert validated data → return 201
↓ bad JSON ↓ invalid fields ↓ database failure
400 400 safe error responseEvery rejection must return before insertion. Sending an error message but continuing the function can still write bad data.
TypeScript is not input validation
A TypeScript type describes what your program expects. An HTTP caller does not have to respect it. as Product merely asks TypeScript to trust you; it does not inspect incoming JSON. Zod checks values at runtime, where a missing name, string price or negative number can actually be rejected.
Study this generic schema. It is a small pattern to adapt, not your complete database endpoint:
import { z } from 'zod';
const productSchema = z.object({
name: z.string().trim().min(1).max(120),
price: z.number().finite().min(0)
}).strict();trim removes outer whitespace; min(1) rejects an empty name after trimming. finite rejects non-finite numbers. strict rejects unexpected fields, so a client cannot quietly attach ownership or privilege fields to this payload. A numeric JSON value 12 is different from the string "12". Decide deliberately whether to convert user form values before sending; do not silently accept anything that resembles a number.
Parse first, then check, then continue
Here is a complete validation rehearsal that does not write to a database. Put it in a disposable endpoint first, understand it, then build your own product-writing endpoint:
import { json, type RequestHandler } from '@sveltejs/kit';
import { z } from 'zod';
const sampleSchema = z.object({
title: z.string().trim().min(1).max(80)
}).strict();
export const POST: RequestHandler = async ({ request }) => {
let body: unknown;
try {
body = await request.json();
} catch {
return json({ error: 'Send valid JSON.' }, { status: 400 });
}
const result = sampleSchema.safeParse(body);
if (!result.success) {
return json({ error: 'A non-empty title is required.' }, { status: 400 });
}
// A real handler would use result.data only after this point.
return json({ accepted: result.data.title }, { status: 200 });
};request.json() can throw before Zod ever runs because malformed JSON is not a JavaScript value. safeParse then returns either success with validated data or failure with issues. Unlike parse, it does not throw for an ordinary schema failure. json creates a JSON response and the appropriate content type.
Read each early return aloud. Which inputs can reach the last line? Try {"title":" Note "}, {"title":""}, {"title":12} and a broken body {"title":. The first returns the trimmed title; the other three return 400 for different reasons.
Connect validation to a real write
In your products endpoint, replace the rehearsal schema with fields that match your products table. Use only result.data for the insert. Do not insert the original body after validating: that would discard the validation boundary.
The client for your course practice table is described in the database lesson. Its public credentials cannot bypass RLS, even when used on a server. For a signed-in private app, use the framework's documented per-request, user-session-aware server client; derive ownership from the verified session, never a user_id supplied by the browser. A privileged server key bypasses RLS and must not be used as a beginner shortcut to fix permission failures.
After insertion, check the database client's error result. Return a small confirmation and 201 Created only when the write succeeded. For a database outage, return a safe failure response, not 201. Log a safe diagnostic server-side without exposing credentials, private rows or internal connection details in the response. Validation does not replace authentication, authorisation or rate limiting.
Test JSON without shell-quoting surprises
Create valid.json in your practice project's root using your editor:
{"name":"Practice notebook","price":12}Create invalid.json with a blank name or negative numeric price. These are harmless test files, not credentials. Start your app and open a second terminal in the project root containing those JSON files. Use the app's actual printed local port. On Mac:
curl -i http://localhost:5173/api/products -H "Content-Type: application/json" --data-binary "@valid.json" curl -i http://localhost:5173/api/products -H "Content-Type: application/json" --data-binary "@invalid.json"
On Windows PowerShell, use curl.exe with the same arguments. --data-binary "@valid.json" sends the file's contents and selects POST; sending -d valid.json would send the literal filename instead. The quoted header is one argument. There is no need for platform-specific inline JSON escaping.
Test malformed JSON with a third file. Also try a number as the name and an unknown extra field. Record actual statuses rather than copying expected output. For valid input, expect your write endpoint's success status and verify a new row in the database. For every invalid request, expect 400 and no inserted row.
Diagnose a failed handshake
| Symptom | Check |
|---|---|
| 404 | Exact route path, +server.ts filename, server URL and port |
| 405 | Did you export POST, and send POST rather than GET? |
| 400 for every input | File contents, JSON numbers versus strings, field names and schema |
| 400 but row still appears | Ensure validation failure returns before the insert |
| 500 on valid input | Server log, table columns, configuration and RLS/grants |
| Browser shows success for 400 | Check response.ok in the calling code |
Assessment readiness
Before activity 4.3, explain the difference between malformed JSON and valid JSON with invalid fields; trace the early return; demonstrate one real write and one rejected write; and show that your server inserts validated values only. You should be able to locate the boundary without asking an AI to complete the endpoint for you. Optional Hono middleware follows the same principle: reject early, otherwise call the next handler, and never bypass permissions just because validation passed.
Key terms
- Middleware
- Code that checks every request before your handler.
- Hono
- A tiny, fast framework for server endpoints.
- Zod
- Declares a schema and validates input against it.
- 400 Bad Request
- The status for input that fails validation.
Summary — copy into your notes
- Middleware guards every request.
- Zod schemas reject bad input with a clear 400.
- Server-side validation is the one that counts.