Production-ready contact form backend using TypeScript, AWS Lambda, API Gateway, and AWS SES.
It is designed for secure public form submission with strong abuse controls, predictable behavior under retries, and flexible deployment configuration.
- Strong input validation with Zod
- SES email delivery with typed AWS SDK v3
- CORS allow-list support with wildcard subdomains and multi-origin config
- Rate limiting in-memory by default, optional distributed mode via DynamoDB
- Idempotency-key deduplication to prevent duplicate sends
- Optional CAPTCHA verification for high-volume abuse
- Honeypot trap for low-cost bot filtering
- Structured error responses and explicit HTTP status handling
- Comprehensive automated tests with Vitest
- Runtime: Node.js 20 on AWS Lambda
- Entry point: POST /contact
- Email transport: AWS SES
- Optional data stores:
- Distributed rate limit table (DynamoDB)
- Idempotency table (DynamoDB)
- Node.js 20+
- AWS CLI configured
- AWS SES identity verified for sender email
npm installCreate secrets.json from your sample and set values similar to:
{
"EMAIL": "your-verified-email@example.com",
"DOMAIN": "https://yourwebsite.com",
"AWS_REGION": "us-east-1",
"SES_IDENTITY_ARN": "arn:aws:ses:us-east-1:123456789012:identity/your-verified-email@example.com",
"RATE_LIMIT_MAX_REQUESTS": "5",
"RATE_LIMIT_WINDOW_MS": "60000",
"RATE_LIMIT_TABLE": "contact-form-rate-limit",
"RATE_LIMIT_PARTITION_KEY": "id",
"RATE_LIMIT_FAIL_OPEN": "true",
"IDEMPOTENCY_TTL_MS": "600000",
"IDEMPOTENCY_TABLE": "contact-form-idempotency",
"IDEMPOTENCY_PARTITION_KEY": "id",
"IDEMPOTENCY_FAIL_OPEN": "true",
"CAPTCHA_SECRET": "optional-provider-secret",
"CAPTCHA_VERIFY_URL": "https://challenges.cloudflare.com/turnstile/v0/siteverify",
"CAPTCHA_TOKEN_HEADER": "x-captcha-token",
"CAPTCHA_FAIL_OPEN": "false"
}npm run offlinenpm run deployPOST /contact
{
"name": "John Doe",
"email": "john@example.com",
"content": "Hello, I would like to get in touch.",
"subject": "Website Contact"
}Idempotency-KeyorX-Idempotency-KeyX-Captcha-Token(or custom header viaCAPTCHA_TOKEN_HEADER)
{
"success": true,
"message": "Your message has been sent successfully!",
"messageId": "ses-message-id"
}For duplicate idempotency submissions, returns 200 with header
Idempotency-Replayed: true and does not send another email.
{
"success": false,
"error": "Validation failed",
"details": "Name must be at least 2 characters long"
}200success (or replay acknowledged)400validation/captcha request errors403forbidden (origin/captcha verification failure)405method not allowed429rate limit exceeded500internal server error503captcha provider unavailable when fail-closed
- Strict Zod schema validation
- Character and length constraints
- HTML entity sanitization for user-provided fields
- Suspicious payload pattern detection
- Exact origin allow-list support
- Wildcard subdomains (
*.example.com) - Comma-separated origin configuration
- Proper preflight validation
- Honeypot field (
_honeypot) for naive bot detection - Rate limiting with configurable window and threshold
- Optional distributed rate limiting in DynamoDB
- Optional CAPTCHA challenge verification
- Optional idempotency-key handling
- Prevents retries/double-click duplicate sends
- In-memory or DynamoDB-backed dedupe store
Required:
EMAILverified SES senderDOMAINallowed origin(s) or*AWS_REGIONAWS region
Recommended:
SES_IDENTITY_ARNscope SES IAM permissions to identity ARN
Rate limiting:
RATE_LIMIT_MAX_REQUESTSdefault5RATE_LIMIT_WINDOW_MSdefault60000RATE_LIMIT_TABLEoptional DynamoDB tableRATE_LIMIT_PARTITION_KEYdefaultidRATE_LIMIT_FAIL_OPENdefaulttrue
Idempotency:
IDEMPOTENCY_TTL_MSdefault600000IDEMPOTENCY_TABLEoptional DynamoDB tableIDEMPOTENCY_PARTITION_KEYdefaultidIDEMPOTENCY_FAIL_OPENdefaulttrue
CAPTCHA:
CAPTCHA_SECRETenables verification when setCAPTCHA_VERIFY_URLprovider endpointCAPTCHA_TOKEN_HEADERdefaultx-captcha-tokenCAPTCHA_FAIL_OPENdefaultfalse
Distributed rate limit table:
- Partition key: String (
idby default) - TTL attribute: Number
expiresAt(recommended)
Idempotency table:
- Partition key: String (
idby default) - TTL attribute: Number
expiresAt(recommended)
npm run build
npm run deploy
npm run deploy:dev
npm run deploy:prod
npm run offline
npm run lint
npm run lint:fix
npm run format
npm run type-check
npm run validate
npm test
npm run test:watch
npm run test:ui
npm run test:coveragesrc/
handler.ts
security.ts
validation.ts
errors.ts
types.ts
tests/
examples/
serverless.yml
The test suite covers:
- Handler behavior and response contracts
- Validation rules and edge cases
- Security utilities (origin checks, rate limiting, sanitizer)
- Distributed rate limit behavior and fail-open/fail-closed semantics
- Idempotency and CAPTCHA flow behavior
Run:
npm testCheck first:
- CloudWatch logs for request and error context
- SES sending status and identity verification
- CORS origin configuration (
DOMAIN) - CAPTCHA provider health and token header wiring
- DynamoDB table names, IAM access, and TTL settings
MIT