A cat sitting next to a computer monitor

Fix Astro API route 404 on Cloudflare Workers (/api/contact)


The contact form looked perfect in the browser. Click submit, and the network tab showed POST /api/contact → 404. HTML pages loaded fine. That combination usually means your API route never made it into the Worker bundle, not that Resend or Turnstile failed.

On Astro 6 with @astrojs/cloudflare v13, API routes live in src/pages/api/ and must stay server-rendered (prerender = false). A green build can still deploy a Worker that only serves static assets if the API file was prerendered or never exported.

Symptom: 404 only in production (or only on POST)

Check these quickly:

ObservationLikely cause
GET /contact works, POST /api/contact is 404API route not deployed or wrong path
Works with astro dev, fails after wrangler deployprerender = true on the API file, or build output missing the route
404 in browser but no Worker log lineRequest never hit your script (static asset served instead)

If you see no log at all for /api/contact, read Why Cloudflare Worker logs only show some routes first—then come back here.

Fix 1: API route file location and export

The handler must live at:

src/pages/api/contact.ts

and export the HTTP method you call from the client:

export const prerender = false;

export const POST: APIRoute = async ({ request, locals }) => {
  // ...
};

Without export const POST, Astro will not register a POST handler. Without prerender = false, the route may be treated as static at build time and omitted from the Worker.

My contact page posts JSON to the same origin:

const res = await fetch('/api/contact', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name, email, message, turnstileToken }),
});

Keep the path consistent: file api/contact.ts → URL /api/contact.

Fix 2: Confirm the API route is not static-only

After:

npm run build

check dist/client/:

  • There should be no dist/client/api/contact/ HTML (or similar prerendered API output).
  • You should see pre-rendered pages like dist/client/blog/.../index.html and dist/client/index.html.
  • There should be no dist/client/contact/index.htmlcontact.astro uses prerender = false, so /contact is rendered on the Worker.

If the API path only exists as a static file, POST requests will never reach your handler. Rebuild after changing prerender, adapter, or file location.

Fix 3: Adapter and output mode

This site uses:

adapter: cloudflare({ imageService: 'compile' }),

Do not set output: 'static' if you need server routes. Hybrid/server output with the Cloudflare adapter is what registers API endpoints on the Worker.

If you recently copied a template from a static-only Astro project, you can get a green astro build and still deploy a Worker that only serves assets.

Fix 4: curl the API directly

Bypass the form to isolate routing:

curl -i -X POST "https://mayfield.io/api/contact" \
  -H "Content-Type: application/json" \
  -d '{"name":"Test","email":"test@example.com","message":"API route probe"}'

Interpretation:

  • 404 — routing / deploy / prerender issue (this post)
  • 400 with validation text — route works; payload or Turnstile token issue
  • 403 — Turnstile verification failed
  • 502 with email text — route works; see Fix Resend 403 and 422

Fix 5: Redeploy and confirm Wrangler sees the script

Run a dry-run in CI or locally:

npx wrangler deploy --dry-run

You should see your Worker upload size and bindings (e.g. env.ASSETS, and on Astro 6 often env.SESSION for the adapter). wrangler.json main should be @astrojs/cloudflare/entrypoints/server. If the dry-run fails, production may still be serving an older bundle without the API route.

I run dry-run on every PR for this reason—see Wrangler deploy dry-run in CI.

Fix 6: Trailing slash mismatches

If you enable trailingSlash in astro.config.mjs, use the same shape in the client fetch and in curl tests (/api/contact vs /api/contact/). This site leaves the default and posts to /api/contact without a trailing slash.

When it is not a 404 problem

Once POST returns 400/403/502, the API route exists. Switch troubleshooting:

404s are almost always build output and routing, not email provider configuration.


Hero photo: Midge cat and computer by dougwoods, CC BY 2.0.