Integration guide
Next.js contact form → Nodedr submit API
Keep API keys on the server. The browser only calls same-origin POST /api/contact-submit; Next.js validates input and forwards to api.nodedr.com.
For AI coding assistants
If you use an AI builder (Cursor, GitHub Copilot, ChatGPT, or similar), treat the text in Reuse prompt below as a copy-paste prompt: paste it into a new chat, fill in bracketed placeholders, and ask the tool to implement the pattern. That keeps server keys off the client and matches the Nodedr submit API rules.
In this Submify monorepo the Next.js proxy is already implemented at /api/contact-submit because POST /api/submit is used by the Go API. If you let an assistant "simplify" routes or nginx without that distinction, you can break production. Prefer editing this repo only when you know the impact; use the prompt mainly for other Next.js projects.
Overview
This pattern is for marketing or brochure sites where you want contact submissions delivered through Nodedr's hosted submit API without exposing pk_ / sk_ keys in client bundles. It complements the FormSubmit flow from the same rule file — pick one submission path per form.
- Browser →
fetch('/api/contact-submit')with JSON + honeypot. - Route handler → Zod validation, build
{ data, files: [] }, optional HMAC over the exact UTF-8 body. - Upstream →
POST https://api.nodedr.com/api/submitwithx-api-keyand optionalx-signature.
Path in this monorepo
Submify's Go API already exposes POST /api/submit behind nginx. A Next.js route at the same path would never be reached in production.
/api/contact-submit. Nginx proxies that path to the web container before the generic /api/ → Go rule. On next dev, the route works directly on port 3000.Request flow
- User submits the contact form on the homepage (or any client component).
- Client sends JSON:
name,email,message, optionalcompany, andgotcha(must be empty — hidden field). - Route returns
{ ok: true }or{ error: "..." }; map upstream failures to 502 with a safe message.
Environment variables
Set on the web service at runtime (see apps/web/.env.example and docker-compose.yml):
| Variable | Required | Notes |
|---|---|---|
NODEDR_SUBMIT_PUBLIC_KEY or NODEDR_PUBLIC_KEY | Yes (for a working form) | Must start with pk_. Not a NEXT_PUBLIC_* variable. |
NODEDR_SUBMIT_SECRET_KEY | No | If set (sk_...), route adds x-signature (hex HMAC-SHA256 of the exact body string). |
Never commit real secret keys. Use placeholders in docs and examples only.
Files in this repository
apps/web/app/api/contact-submit/route.ts— POST handler, upstream fetch.apps/web/lib/nodedrSubmitEnv.ts— reads env at runtime.apps/web/lib/contactSubmitSchema.ts— shared Zod schema +ContactSubmitPayloadtype.apps/web/lib/contactSubmitPath.ts— single constant for the clientfetchpath.apps/web/components/landing/contact-form.tsx— example wired form.infra/nginx/nginx.conf—location /api/contact-submit→ Next.js.apps/web/Dockerfile— copiespublic/into the standalone image (fixes missing logo assets).
Content Security Policy
The browser only talks to same origin for this flow. If you add a strict CSP, include connect-src 'self' (or your API host) so fetch('/api/contact-submit') is allowed. You do not need to allow api.nodedr.com in the browser CSP — that call is server-side.
Copy-paste prompt for new projects
AI builders: copy everything inside the box below into your assistant chat as the prompt, then adjust bracketed paths and paths/fetch URLs as explained in For AI builders above.
Use this in Cursor (or any assistant) to recreate the pattern on another Next.js App Router repo. Replace bracketed paths and, for this monorepo, swap /api/submit → /api/contact-submit in the generated client code.
Prompt you can reuse in chat
Copy and adjust the bracketed parts:
In this repo's Next.js App Router site at [path/to/site-folder], implement contact form submission using the Nodedr submit API proxy pattern (same as SeattleDrainCleaningCo), not FormSubmit in the browser.
Requirements:
1. Add `src/app/api/submit/route.ts` that accepts POST JSON, validates with a shared Zod schema (honeypot field e.g. gotcha must be empty), builds the upstream JSON payload, and POSTs to `https://api.nodedr.com/api/submit` with `Content-Type: application/json`, header `x-api-key` set from server env (`NODEDR_SUBMIT_PUBLIC_KEY` or `NODEDR_PUBLIC_KEY`, value must be `pk_...`). If `NODEDR_SUBMIT_SECRET_KEY` (`sk_...`) is set, add `x-signature`: hex HMAC-SHA256 of the exact UTF-8 body string you send upstream.
2. Add `src/lib/nodedrSubmitEnv.ts` (or equivalent) that reads those env vars at runtime (no `NEXT_PUBLIC_` for secrets).
3. Add `src/lib/contactSubmitSchema.ts` shared between client and route; export the inferred type.
4. Wire the contact form(s) to `fetch("/api/submit", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify({ ...fields, gotcha }) })`, show inline success/error, never expose keys to the client.
5. Ensure CSP `connect-src` allows `'self'` for this fetch if the project uses CSP.
6. Document env vars in `.env.example` (public key name only as a placeholder; never commit real `sk_`).
Follow `f:/code/.cursor/rules/15-formsubmit-and-contact-forms.mdc` (Nodedr submit API section) and match file layout/naming to SeattleDrainCleaningCo unless this site's structure differs—then adapt minimally.
That gives a future session enough context to recreate the pattern without re-explaining it.Canonical rules reference: 15-formsubmit-and-contact-forms.mdc (Nodedr submit API section).
