serverless-contact-form
v2.0.0
Published
A modern TypeScript serverless contact form API using AWS Lambda and SES
Maintainers
Readme
Serverless Contact Form API
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.
Highlights
- 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
Architecture
- 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)
Quick Start
Prerequisites
- Node.js 20+
- AWS CLI configured
- AWS SES identity verified for sender email
Install
npm installConfigure
Create secrets.json from your sample and set values similar to:
{
"EMAIL": "[email protected]",
"DOMAIN": "https://yourwebsite.com",
"AWS_REGION": "us-east-1",
"SES_IDENTITY_ARN": "arn:aws:ses:us-east-1:123456789012:identity/[email protected]",
"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"
}Run Locally
npm run offlineDeploy
npm run deployAPI Contract
Endpoint
POST /contact
Request Body
{
"name": "John Doe",
"email": "[email protected]",
"content": "Hello, I would like to get in touch.",
"subject": "Website Contact"
}Optional Headers
Idempotency-KeyorX-Idempotency-KeyX-Captcha-Token(or custom header viaCAPTCHA_TOKEN_HEADER)
Success Response
{
"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.
Error Response
{
"success": false,
"error": "Validation failed",
"details": "Name must be at least 2 characters long"
}Status Codes
200success (or replay acknowledged)400validation/captcha request errors403forbidden (origin/captcha verification failure)405method not allowed429rate limit exceeded500internal server error503captcha provider unavailable when fail-closed
Security Model
Validation and Sanitization
- Strict Zod schema validation
- Character and length constraints
- HTML entity sanitization for user-provided fields
- Suspicious payload pattern detection
Origin and CORS
- Exact origin allow-list support
- Wildcard subdomains (
*.example.com) - Comma-separated origin configuration
- Proper preflight validation
Abuse Controls
- Honeypot field (
_honeypot) for naive bot detection - Rate limiting with configurable window and threshold
- Optional distributed rate limiting in DynamoDB
- Optional CAPTCHA challenge verification
Duplicate Submission Protection
- Optional idempotency-key handling
- Prevents retries/double-click duplicate sends
- In-memory or DynamoDB-backed dedupe store
Configuration Reference
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
DynamoDB Table Notes
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)
Development
Scripts
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:coverageProject Structure
src/
handler.ts
security.ts
validation.ts
errors.ts
types.ts
tests/
examples/
serverless.ymlTesting
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 testOperations and Troubleshooting
Check 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
License
MIT
