API routes: the doorbell between places
How the browser asks the server for things: routes as defined questions with defined answers, the read-and-write set Replyable needs, and reading a route without fear.
The browser can't touch the database (rightly — anyone can open the browser's code). So the server exposes API routes: named doors, each accepting a specific question and returning a specific answer. In Next.js these are just files — app/api/messages/route.ts becomes the door at /api/messages — and Replyable needs five doors:
GET /api/messages -> list messages (optional ?status= filter)
GET /api/messages/[id] -> one message + its replies
PATCH /api/messages/[id] -> update status (body: { status })
POST /api/replies -> save a reply (body: { message_id, body,
was_ai_drafted })
POST /api/ai/draft -> Module 5's door - reserved, not built yet
POST /api/ai/summarize -> also Module 5's - reserved (cached summaries)
GET reads. POST creates. PATCH edits. That's the grammar.
Every door returns JSON - your data as labeled pairs in braces, the
plain-text format servers answer in: data on success, { error } + a
status code on failure. (The AI draft door will bend the JSON rule in
Module 5 - it streams.)Reading a route (same anatomy every time)
import { db } from "@/lib/db"; // the database connection
import { NextResponse } from "next/server";
export async function GET(request: Request) {
// 1. read the question's details (the ?status= filter, if any)
const status = new URL(request.url).searchParams.get("status");
// 2. do the work - placeholders, never pasted values
const rows = status
? db.prepare("SELECT * FROM messages WHERE status = ? ORDER BY received_at DESC").all(status)
: db.prepare("SELECT * FROM messages ORDER BY received_at DESC").all();
// 3. answer in JSON
return NextResponse.json(rows);
}Read the question → do the work → answer — every route you'll ever generate has those three beats, and reviewing one means checking each beat: does it read the right inputs? does the work use placeholders and match the spec? does the answer include what the screen needs (and not include what it shouldn't — a lesson that matters more once apps have users and secrets)? Errors get the same discipline: bad input answers 400 with a message, missing rows answer 404, and the server never answers a crash page where JSON was promised.
Every POST/PATCH body gets checked before touching the database: status must be one of your three values, reply body non-empty and under a sane length, message_id must exist. The browser's form already prevents most bad input — and that protection is cosmetic, because anyone can knock on /api/replies directly with any payload they like. The door checks or nobody does. This single habit is most of Module 6's security story arriving early.