Dynamic Content & Forms

Website Rebuild with Astro

Chapter 8 · Dynamic Content & Forms

Django needed a separate ModelForm class. Laravel needed a separate FormRequest class. Rails needed Strong Parameters, at least filtering which fields are allowed. Astro needs none of that structurally — but the reason is worth being honest about.

The Endpoint, With No Validation at All

// src/pages/api/pages/[id]/title.ts import type { APIRoute } from 'astro'; import { db } from '../../../../db/client'; import { pages } from '../../../../db/schema'; import { eq } from 'drizzle-orm'; export const POST: APIRoute = async ({ params, request }) => { const { title } = await request.json(); await db.update(pages).set({ title }).where(eq(pages.id, Number(params.id))); return new Response(JSON.stringify({ status: 'ok' })); };
The leanest answer, for a real reason
Nothing here checks that title is even a string, that it exists at all, or that it's a reasonable length. A request body of { "title": 12345 } or {} is happily passed straight into a real database write. This isn't a smarter, more minimal design than Rails or Laravel's own answers — it's simply what's left when a framework provides no validation layer at all.

Closing the Gap: Zod, Deliberately

import { z } from 'zod'; const titleSchema = z.object({ title: z.string().min(1).max(255), }); export const POST: APIRoute = async ({ params, request }) => { const body = await request.json(); const result = titleSchema.safeParse(body); if (!result.success) { return new Response(JSON.stringify({ error: result.error.flatten() }), { status: 400 }); } await db.update(pages).set({ title: result.data.title }).where(eq(pages.id, Number(params.id))); return new Response(JSON.stringify({ status: 'ok' })); };

The same Zod library already used for Content Collections in astro1 Chapter 5 closes this gap. safeParse returns a typed, validated result rather than throwing — the same discipline Zod's own schema already demonstrated, applied here to a plain API route instead of stored content.

The Deliberate No-Auth-Check-Yet Gap — the Bare Minimum Version

Matching every sibling course's own Chapter 8, this endpoint has no authentication check yet, to be closed in Chapter 9. Astro's own version of the gap is genuinely the most minimal of all five: Rails left a missing before_action filter, Laravel left a visible authorize() placeholder — Astro has no conventional place for either concept to even go yet, since there's no framework-level auth mechanism at all.

A Real, Unaddressed Limitation: No CSRF Protection

Not solved by this course, named honestly instead
Rails ships protect_from_forgery. Laravel ships VerifyCsrfToken middleware. Django ships its own CSRF middleware. Astro ships none of that — a request from any origin can hit this endpoint with no built-in same-origin or token check standing in the way. A production version of this project would need to add that protection by hand (a same-origin check on the request's own Origin header, or a real CSRF token scheme). This course names the gap honestly rather than building a full solution for it, matching the same "honest scope note" convention every capstone in this series already uses.

Five Structural Answers, Compared

Next.jsDjangoLaravelRailsAstro
Validation layerManual (Zod, by choice)ModelFormFormRequestStrong ParametersManual (Zod, by necessity)
CSRF protection built in?NoYesYesYesNo

Hands-On Exercises

Exercise 1

Build the /api/pages/[id]/title.ts endpoint with no validation, then send a malformed request body (e.g. a numeric title) and confirm it's written to the database completely unchecked.

📄 View solution
Exercise 2

Add the Zod schema and safeParse validation, and confirm the same malformed request is now rejected with a 400 response instead of reaching the database.

📄 View solution
Exercise 3

In a local dev environment, confirm no CSRF protection exists by successfully calling this endpoint from a fetch request issued from a different origin/port, with no same-origin check blocking it.

📄 View solution

Chapter 8 Quick Reference

  • The leanest endpoint of all five siblings — no separate validation class, but only because none is provided by default
  • Zod's safeParse — the deliberate fix, reused from astro1 Chapter 5's own Content Collections
  • The gap looks different here — no conventional place for an auth check to even go yet, closed in Chapter 9
  • No CSRF protection at all — a real, unaddressed limitation, named honestly rather than solved
  • Next chapter: Admin Authentication