@adamarant/contact-form
v0.1.1
Published
Drop-in contact form + secured Resend email flow (Cloudflare Turnstile, honeypot/time-trap, rate-limit). Configure fields, recipient and keys per project. Next.js or any fetch runtime.
Maintainers
Readme
@adamarant/contact-form
Drop-in contact form + secured Resend email flow for Next.js (or any Fetch runtime). Install once, then per project you only customize three things: the fields, the recipient, and the API keys.
Security is built in: Cloudflare Turnstile, honeypot + time-trap, per-IP rate-limiting, and strict server-side validation. Standalone — no design system or database required (both are optional).
Install
npm install @adamarant/contact-formPeer deps are optional and pulled in only if you use them: react (client form),
@upstash/redis (durable rate-limit). resend is bundled.
Server — the secured route
Drop a single handler into an App Router route. It runs the full pipeline
(parse → honeypot → validate → rate-limit → Turnstile → optional persist → email)
and returns { success: true } or { success: false, error }.
// src/app/api/contact/route.ts
import { createContactRoute } from '@adamarant/contact-form/server'
export const POST = createContactRoute({
fields: [
{ name: 'name', label: 'Name', required: true, autoComplete: 'name' },
{ name: 'email', label: 'Email', type: 'email', required: true, autoComplete: 'email' },
{ name: 'message', label: 'Message', type: 'textarea', required: true },
],
resend: {
apiKey: process.env.RESEND_API_KEY!, // keys live in the consuming app, never in this lib
from: 'Acme <[email protected]>', // domain must be verified in Resend
to: '[email protected]',
},
brand: { name: 'Acme', url: 'https://acme.com' },
// Optional security layers — omit any you don't want:
turnstile: { secretKey: process.env.TURNSTILE_SECRET_KEY! },
rateLimit: {
redis: {
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
},
maxAttempts: 5,
windowMs: 60 * 60 * 1000,
},
// Optional: persist the lead before the email is sent (DB / CRM).
// onSubmit: async ({ data, ip, userAgent }) => { await db.insert(...) },
})Omit rateLimit.redis to use the in-memory limiter (per-process; fine for a
single instance, multiplied per instance on serverless). Pass rateLimit: false
to disable. Omit turnstile to skip the captcha.
Client — the ready form
'use client'
import { ContactForm } from '@adamarant/contact-form/client'
import '@adamarant/contact-form/styles' // optional default styling
const fields = [
{ name: 'name', label: 'Name', required: true },
{ name: 'email', label: 'Email', type: 'email', required: true },
{ name: 'message', label: 'Message', type: 'textarea', required: true },
] as const
export function Contact() {
return (
<ContactForm
action="/api/contact"
fields={fields}
turnstileSiteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY}
/>
)
}The form ships the honeypot, the Turnstile widget (self-loaded, no extra dependency), the submit state machine, and success/error UI.
Styling
Two options:
Import the default stylesheet (
@adamarant/contact-form/styles) and tune the--acf-*variables.Map to your own classes via the
classNamesprop — e.g. a design-system consumer:<ContactForm action="/api/contact" fields={fields} classNames={{ input: 'ds-input', textarea: 'ds-textarea', label: 'ds-label', button: 'ds-btn ds-btn--full' }} />
Localized copy
<ContactForm
action="/api/contact"
fields={fields}
locale="it"
messages={{
submit: 'Invia',
sending: 'Invio…',
successTitle: 'Messaggio inviato',
successMessage: 'Grazie, ti rispondiamo presto.',
errors: { rate_limited: 'Troppi tentativi, riprova più tardi.' },
}}
/>Error codes returned by the route: missing_fields, invalid_email,
too_long, rate_limited, captcha_failed, invalid_request, server_error.
Email template
The default template renders a clean notification (inline fields as a table,
textarea/block fields as full-width boxes, a Reply button targeting the
submitter). Customize via brand, subject, intro, or replace it entirely
with render. You can also call renderContactEmail from
@adamarant/contact-form/email directly.
Resend notes
- Each project sets its own
RESEND_API_KEY+ verifiedfromdomain. One Resend plan covers a fixed number of verified domains (e.g. the €20 tier = 10 domains). - With no API key configured, sends are a no-op "mock" success — handy in dev.
API surface
| Entry | Exports |
|-------|---------|
| @adamarant/contact-form/server | createContactRoute, plus primitives: verifyTurnstileToken, validateHoneypot, createRateLimiter, validateContact, sendContactEmail, getClientIp |
| @adamarant/contact-form/client | ContactForm, Turnstile |
| @adamarant/contact-form/email | renderContactEmail |
| @adamarant/contact-form | shared types + HONEYPOT_FIELD_NAMES |
| @adamarant/contact-form/styles | optional default CSS |
License
MIT
