AI Global Academy Join the waitlist

Courses / Backend: leads, data, notifications

Lesson 4.3 · 55 minThe lead endpoint

Duration~55 min in the lesson + ~25 min homework
PrerequisitesCheckpoint lesson-4.2.
Checkpointlesson-4.3

What you will have

The form on your live domain saves real leads: a valid submission becomes a row in the leads table, and invalid data sent straight to the endpoint is rejected with a clear error.

Video

The video for this lesson is not recorded yet.

Prompts used in this lesson

Part 4 — Build the endpoint

Prompt to Claude
Purpose: make the lead form save real leads. Replace the temporary fake submission from lesson 3.7 with a real server endpoint.

Context: Next.js App Router project at checkpoint lesson-4.2. The leads table exists in Supabase (see supabase/migrations/ and the Database section of docs/architecture.md). There is a server-only Supabase helper that uses the secret key. Read the lead form component and its browser validation before you start. Do not open or print .env.local.

Done looks like:
- A route handler at /api/leads that accepts POST with a JSON body.
- The validation rules live in one shared module that both the form and the endpoint use, so the rules cannot drift apart.
- The endpoint validates every field on the server: required fields, formats, and a sensible maximum length for each field. It trims whitespace and lowercases the email.
- It accepts only the fields the visitor fills in. Anything else in the body is ignored. status is always set to 'new' by the server. The source and scoring columns stay empty for now; section 6 fills them.
- Responses: 201 with {"ok": true} on success; 400 with a per-field error list on invalid data or unreadable JSON; 500 with a generic message if saving fails, with the real error written to the server log but no personal data in the log; any method other than POST gets 405.
- The form sends the real request, keeps its sending/success/error states from 3.7, shows field errors returned by the server, and goes to /thank-you only after a 201.
- The fake submission code is removed completely.
- docs/architecture.md describes the endpoint and its responses.

Constraints: the secret key and the server helper must never be imported into browser code. Use the Web Request/Response APIs of route handlers; add a small validation library only if you need it and tell me which. Do not add spam protection or notifications yet — those are the next two lessons.

Before you report: start the dev server and test with real requests — one valid submission, one with a missing required field, one with a malformed email, one with a 5,000-character name, one with extra fields including status "won", and one GET. Give every test lead a name starting with "TEST". Show me a table of request, status code and response. Then run the production build.

Part 6 — Go around the form

Prompt to Claude
Purpose: I want to send requests straight to the lead endpoint, bypassing the form, to prove the server validates on its own.

Done looks like: a script run as `npm run test:endpoint -- <base-url>` that sends these POST requests to <base-url>/api/leads and prints one line per request with the status code and the response body: (1) empty body, (2) text that is not JSON, (3) valid JSON with a missing required field, (4) a malformed email, (5) a 5,000-character message beyond the limit, (6) a fully valid lead named "TEST direct request". It finishes with PASS if 1–5 returned 400 and 6 returned 201, otherwise FAIL.

Constraints: the script needs no keys and reads no env files. It only does what any stranger on the internet could do.

Before you report: run it against the local dev server and show the output.

Do along

Pause the video where a step says so and do it on your own project.

  1. Before you start: lesson-4.2 with your own Supabase variables in place.
  2. Pause after Part 4. Give Claude the endpoint prompt from Part 4. If your form has different fields from the sample, you do not need to change the prompt: Claude reads the form. If your business needs a rule the sample lacks (for example "phone is required, email is optional"), add it under "Done looks like".
  3. Read Claude's test table. Ask about any line you do not understand before accepting.
  4. Pause after Part 5. Submit one valid lead through the local form with the Network tab open. Find the request, the payload, the status and the response.
  5. Check the new row in the Supabase table view.
  6. Pause after Part 6. Give Claude the script prompt from Part 6 and run the script locally.
  7. Pause after Part 7. Commit and push. Wait for the deployment to finish.
  8. Submit one lead on your live domain, then run the script against the live domain.

Check your work

  1. On the live domain, submit a valid lead named "TEST Live". Expected: status 201 in the Network tab, then the thank-you page.
  2. Refresh leads in the Supabase table view. Expected: the "TEST Live" row with status new.
  3. Run npm run test:endpoint -- https://<your-domain>. Expected: requests 1–5 return 400, request 6 returns 201, final line PASS.
  4. Open https://<your-domain>/api/leads in the address bar. Expected: status 405, no data shown.
  5. Search the project for the fake submission from 3.7 (ask Claude: "is any part of the fake submission left?"). Expected: none.

Common problems

  • Works locally, 500 on the live site → the Supabase variables are missing in Vercel, or were added after the last deployment → check the three names under Environment Variables in Vercel, then redeploy; variables apply only to new deployments.
  • 500 with "permission denied for table leads" in the server log → the grant to service_role from 4.2 is missing → tell Claude the exact message; it should add a migration with the grant, and you push it with npx supabase db push.
  • Every submission returns 400 → the form's field names and the shared rules disagree → paste the 400 response body to Claude (it contains field names, not secrets) and ask it to align form and schema.
  • The form shows success but no row appears → the fake submission is still in use → look for a POST to /api/leads in the Network tab; if there is none, tell Claude the form does not call the endpoint.
  • Build fails with a message about server-only code in a client component → the secret-key helper was imported into browser code → tell Claude: "the server helper is imported in a client file; move the database call into the route handler".
  • Rows appear twice → the button can be pressed twice while sending → tell Claude: "disable the submit button while the request is in flight and test a double click".

Homework

About 25 minutes. Keep written answers in your own notes, outside the project folder. Give every test lead a name starting with TEST.

  1. Your rules, on paper. Ask Claude to list, in plain language, the rules in the shared validation module: which fields are required, the formats, the maximum lengths. Compare the list with how your customers really write names, phone numbers and messages. Done when: your notes have one line per field saying "fits" or what a real customer would trip over. Change no rules now.
  2. Error messages in your voice. Ask Claude to list every message a visitor can see when the form or the endpoint rejects input. Rewrite the ones that sound unlike your business, have Claude apply your wording, and run npm run test:endpoint against the local server. Done when: the script still ends in PASS and you have read each message on the form. Commit without a tag.
  3. A real device. On your phone, on mobile data rather than your home network, submit a lead named "TEST Phone" on your live domain. Done when: the thank-you page appears on the phone and the row is in the Supabase table view.

Save your work

git add -A
git commit -m "Lesson 4.3: lead endpoint saves form submissions"
git tag lesson-4.3
git push
git push --tags