Validate every lead submission on the server, even if the browser also checks required fields and email format. For the Next.js App Router, a Server Action can parse the submitted FormData, validate it with a schema, return field errors for display, and stop before sending email or calling a CRM when the input is invalid. Add a rate limit to the submission operation before it triggers costly or externally visible work.
Choose the form handler that fits your router
| Architecture | Handler | What to keep in mind |
|---|---|---|
| App Router | Server Action | Next.js documents form submissions handled by Server Actions, with server-side schema validation and field errors returned for display through useActionState. See the Next.js Forms guide. |
| Pages Router | API Route | Next.js documents API Routes as a server-side form-handling option. Apply the same server-side validation and abuse controls; see the Next.js Forms guide. |
The code below illustrates the App Router pattern. It assumes the project has Zod installed and that checkLeadLimit is backed by a rate limiter appropriate to the deployment. The example deliberately leaves the limiter and email or CRM integration as application-specific functions rather than implying a universal threshold or provider setup.
Validate on the server, then return field errors
Browser checks such as required and type="email" improve immediate feedback, but users can bypass them and send a request directly. Treat a Server Action as a public endpoint: Next.js says Server Functions are reachable through direct POST requests. Perform the checks this operation needs inside the action, before any mutation or downstream work. See Next.js Forms and Next.js Mutating Data.
A practical schema checks required values, type, length, and semantic constraints. Allow legitimate Unicode and punctuation in names and messages; impose a message length suited to the form. Use a maintained email validator appropriate to the application rather than treating a simplistic pattern as proof of validity. A syntactically valid address does not prove mailbox access. Validation also does not replace parameterized database operations or context-aware output encoding. See the OWASP Input Validation Cheat Sheet.
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Scan for outdated or missing drivers - takes under a minute3Clear out junk files and repair common Windows errors#1 Best Overall
// app/actions.ts
'use server'
import { z } from 'zod'
const leadSchema = z.object({
name: z.string().trim().min(1, 'Enter your name.').max(100, 'Name is too long.'),
email: z.string().trim().email('Enter a valid email address.').max(254, 'Email address is too long.'),
message: z.string().trim().min(1, 'Enter a message.').max(5000, 'Message is too long.'),
})
type LeadState = {
fieldErrors?: Record<string, string[] | undefined>
status?: 'success' | 'error'
message?: string
}
export async function submitLead(
_previousState: LeadState,
formData: FormData,
): Promise<LeadState> {
// Check the endpoint-level rate limit here, before expensive downstream work.
const limited = await checkLeadLimit()
if (limited) {
return { status: 'error', message: 'Too many submissions. Please try again later.' }
}
const parsed = leadSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
message: formData.get('message'),
})
if (!parsed.success) {
return {
status: 'error',
fieldErrors: parsed.error.flatten().fieldErrors,
}
}
// Only validated values reach email, CRM, webhook, or persistence code.
await sendLeadToYourBackend(parsed.data)
return { status: 'success', message: 'Thanks — your message has been received.' }
}
checkLeadLimit and sendLeadToYourBackend are placeholders for functions you implement. If the limit is exceeded, an API-style handler should respond with HTTP 429 Too Many Requests. A Server Action returns action state rather than giving this example a custom HTTP response path; choose an API Route if your application needs to define that response explicitly. OWASP documents 429 for rate-limited requests in its REST Security Cheat Sheet.
Display the action state in a Client Component
Use useActionState to connect the action to the form and render returned errors. Keep labels and error messages associated with their fields so visitors can identify and correct a problem.
Rank #2
- HTML CSS Design and Build Web Sites
- Comes with secure packaging
- It can be a gift option
// app/lead-form.tsx
'use client'
import { useActionState } from 'react'
import { submitLead } from './actions'
const initialState = {}
export function LeadForm() {
const [state, formAction, pending] = useActionState(submitLead, initialState)
return (
<form action={formAction}>
<label htmlFor="name">Name</label>
<input id="name" name="name" required maxLength={100} />
{state.fieldErrors?.name?.map((error) => <p key={error}>{error}</p>)}
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" required maxLength={254} />
{state.fieldErrors?.email?.map((error) => <p key={error}>{error}</p>)}
<label htmlFor="message">Message</label>
<textarea id="message" name="message" required maxLength={5000} />
{state.fieldErrors?.message?.map((error) => <p key={error}>{error}</p>)}
{state.message && <p role="status">{state.message}</p>}
<button type="submit" disabled={pending}>
{pending ? 'Sending…' : 'Send'}
</button>
</form>
)
}
The HTML constraints are visitor-facing conveniences, not substitutes for the schema. Use an email confirmation or verification flow if the business requirement is to establish mailbox control; format validation alone cannot do that.
Limit request size before parsing, then limit fields
Next.js documents a default Server Action body limit of 1 MB, intended to limit resource consumption; the value can be configured. That is a framework ceiling, not a suitable target for an ordinary lead message. Configure an appropriately tight request-size bound for the application and enforce much smaller field-level limits in the schema. The Next.js serverActions configuration reference describes the default and configuration option; OWASP also recommends input-size bounds in its Input Validation Cheat Sheet.
Rank #3
Set limits as early as your hosting and request-handling stack allows, before buffering or parsing an oversized body. The exact configuration point depends on the deployment path; do not assume a field-level check can prevent the cost of receiving a huge request.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Design rate limits around the operation and deployment
A lead submission may send email, call an external service, deliver a webhook, or start expensive work. OWASP identifies such operations as possible resource-exhaustion or spam vectors. Limit the lead-submission feature itself, and consider additional controls around especially costly downstream operations; a broad site-wide limit alone may not protect the specific work. See the OWASP Business Logic Security Cheat Sheet.
Rank #4
- Brand: Wiley
- Set of 2 Volumes
- A handy two-book set that uniquely combines related technologies Highly visual format and accessible language makes these books highly effective learning tools Perfect for beginning web designers and front-end developers
Choose the counter scope
| Counter approach | Behavior | Trade-off |
|---|---|---|
| In-process counter | Counts requests seen by one running process. | It may not represent traffic across multiple instances or serverless invocations, so it is not a reliable shared limit in a horizontally scaled deployment. |
| Shared backing service | Coordinates limit state across instances. | Assess latency, availability behavior, cost, and operational needs for the actual deployment. |
Upstash documents an HTTP-based Ratelimit library with Next.js and serverless examples, including multiple-limit capabilities. It is one possible implementation, not a requirement: see the Upstash Rate Limit overview.
Set and tune a policy without guessing
There is no evidence-backed universal requests-per-IP, per-email, or time-window threshold for every lead form. Pick limits based on expected traffic, observed abuse, the cost of the downstream action, and whether your deployment shares counter state. Revisit the policy as traffic and abuse patterns change. Avoid relying on a single identifier as a complete defense: legitimate users may share networks, and identifiers can be manipulated.
Recommended Free Tools
Quick Recap
Best Value
Harden the endpoint and protect submitted data
- Review Server Action origin handling. Next.js compares the request
OriginwithHostorX-Forwarded-Hostfor Server Actions as a CSRF risk reduction. If a proxy or multi-layer deployment changes the apparent host, setserverActions.allowedOriginsonly to the safe origins actually required; do not widen the list casually. See the configuration reference. - Keep data access and rendering safe. Use parameterized database operations and encode untrusted text for its output context; validation is not an injection defense by itself.
- Minimize logs and retention. Keep only useful failure metadata for diagnosing rejected requests. Avoid logging full request bodies, secrets, or verbatim rejected input, which can expose sensitive data or create log-injection risks. Set retention appropriate to the lead data you collect; see the OWASP Input Validation Cheat Sheet.
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

