- Feature: Contact Form with D1 Database Integration
- Target Completion: 2-3 hours
- Confidence Score: 9/10 - Well-documented pattern with clear implementation path
- Created: 2025-11-09
A production-ready contact form feature that stores submissions in a Cloudflare D1 (SQLite) database. Includes full CRUD API endpoints, React form component with validation, and proper error handling. This example demonstrates best practices for D1 integration including migrations, SQL injection prevention, and type safety.
-
Pattern: Worker API Handler
- Location:
worker/index.ts:1-12 - Description: Basic worker with URL routing pattern
- Relevance: Foundation for adding contact API routes
- Location:
-
Pattern: TypeScript Configuration
- Location:
tsconfig.worker.json - Description: Worker-specific TypeScript config
- Relevance: Ensures types work correctly in worker context
- Location:
-
Convention: Response.json for API responses
- Example:
worker/index.ts:6-8 - Application: Use Response.json for all contact API responses
- Example:
-
Convention: URL pathname matching for routes
- Example:
url.pathname.startsWith("/api/") - Application: Route contact endpoints under
/api/contacts
- Example:
- Test Framework: Not currently configured
- Pattern: Manual testing via curl commands
- Location: Development workflow
-
Resource: Cloudflare D1
- URL: https://developers.cloudflare.com/d1/
- Key Sections: Get started, Client API, Migrations
- Version: Latest (Workers runtime)
- Gotchas: Local vs production databases are separate; migrations must run on both
-
Resource: D1 Client API
- URL: https://developers.cloudflare.com/d1/platform/client-api/
- Key Sections: Prepared statements, Parameter binding, Transactions
- Version: Latest
- Gotchas: Always use
.bind()for parameters, never string concatenation
-
Resource: Wrangler D1 Commands
- URL: https://developers.cloudflare.com/workers/wrangler/commands/#d1
- Key Sections: create, migrations, execute
- Version: Wrangler 3.x+
- Gotchas:
--localflag needed for local development
-
Example: D1 CRUD API
- Source: Cloudflare D1 documentation examples
- Relevance: Pattern for prepared statements and error handling
- Cautions: Don't trust user input; always validate
-
Example: React Form Validation
- Source: React 19 patterns
- Relevance: Modern form handling with hooks
- Cautions: Validate on both client and server
-
Practice: Parameterized Queries
- Why: Prevents SQL injection attacks
- How: Always use
.bind()with prepared statements - Warning: Never concatenate user input into SQL strings
-
Practice: Server-side Validation
- Why: Client-side validation can be bypassed
- How: Validate all inputs in worker before database operations
- Warning: Don't rely solely on TypeScript types
-
Practice: Pagination
- Why: Prevents performance issues with large datasets
- How: Use LIMIT and OFFSET with sensible defaults
- Warning: Always enforce maximum limits
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ React │ POST │ Worker │ SQL │ D1 │
│ ContactForm │ ───────>│ API Handler │ ───────>│ Database │
│ │ │ │ │ │
│ │<─────── │ │<─────── │ │
│ │ JSON │ │ Rows │ │
└─────────────┘ └──────────────┘ └─────────────┘
Routes:
- POST /api/contacts → createContact()
- GET /api/contacts → listContacts()
- GET /api/contacts/:id → getContact()
- DELETE /api/contacts/:id → deleteContact()
- Purpose: Define the contacts table structure
- Location:
examples/database/d1-contact-form/schema.sql - Dependencies: None
- Interface: SQL schema with indexes and triggers
- Purpose: Versioned database schema changes
- Location:
examples/database/d1-contact-form/migrations/0001_create_contacts.sql - Dependencies: D1 database
- Interface: SQL migration with up/down scripts
- Purpose: Type definitions for contacts and API responses
- Location:
examples/database/d1-contact-form/types.ts - Dependencies: None
- Interface: Exported interfaces and validation functions
- Purpose: Handle HTTP requests and database operations
- Location:
examples/database/d1-contact-form/worker-endpoint.ts - Dependencies: Types, D1 binding
- Interface: Request handlers and router function
- Purpose: User interface for contact form
- Location:
examples/database/d1-contact-form/ContactForm.tsx - Dependencies: Types, Worker API
- Interface: React functional component with hooks
// Database row (snake_case, matches SQLite schema)
interface ContactRow {
id: number;
name: string;
email: string;
message: string;
created_at: string;
updated_at: string;
}
// API model (camelCase, for JavaScript/TypeScript)
interface Contact {
id: number;
name: string;
email: string;
message: string;
createdAt: Date;
updatedAt: Date;
}
// Request payload
interface CreateContactRequest {
name: string;
email: string;
message: string;
}
// API responses
interface ContactResponse {
success: true;
data: Contact;
}
interface ContactListResponse {
success: true;
data: Contact[];
count: number;
}
interface ErrorResponse {
success: false;
error: string;
message: string;
}-
Endpoint:
POST /api/contacts- Purpose: Create a new contact
- Request:
{ name, email, message } - Response:
{ success: true, data: Contact } - Authentication: None (public form)
-
Endpoint:
GET /api/contacts- Purpose: List all contacts with pagination
- Request: Query params
limit,offset - Response:
{ success: true, data: Contact[], count: number } - Authentication: None (could add in production)
-
Endpoint:
GET /api/contacts/:id- Purpose: Get a single contact
- Request: ID in URL path
- Response:
{ success: true, data: Contact } - Authentication: None
-
Endpoint:
DELETE /api/contacts/:id- Purpose: Delete a contact
- Request: ID in URL path
- Response:
{ success: true } - Authentication: None (should add in production)
- Cloudflare account with Workers enabled
- Wrangler CLI installed (
npm install -g wrangler) - Node.js 18+ and npm
- Project set up with Vite + React + TypeScript
Goal: Provision a D1 database in Cloudflare
Commands:
npx wrangler d1 create contacts-dbFiles to Create/Modify:
wrangler.jsonc- Add D1 binding configuration
Validation: Database ID appears in wrangler output
Goal: Connect D1 database to the worker
Files to Create/Modify:
wrangler.jsonc- Add[[d1_databases]]section with binding name "DB"
Pseudocode Approach:
Validation: npm run cf-typegen succeeds and generates DB types
Goal: Define database structure
Files to Create/Modify:
examples/database/d1-contact-form/schema.sql- Full schemaexamples/database/d1-contact-form/migrations/0001_create_contacts.sql- Migration file
Validation: SQL syntax is valid (test with sqlite3)
Goal: Apply schema to database
Commands:
npx wrangler d1 migrations apply contacts-db --local
npx wrangler d1 migrations apply contacts-dbValidation: Tables exist in database (query with wrangler d1 execute)
Goal: Define type-safe interfaces
Files to Create/Modify:
examples/database/d1-contact-form/types.ts- All interfaces and validation
Reference Pattern: Standard TypeScript interface patterns
Validation: No TypeScript errors, types export correctly
Goal: Create CRUD operations
Files to Create/Modify:
examples/database/d1-contact-form/worker-endpoint.ts- All endpoint handlers
Pseudocode Approach:
async function createContact(request, env) {
// 1. Parse and validate request body
// 2. Execute INSERT with .bind() for SQL injection prevention
// 3. Fetch created record
// 4. Return JSON response
}Reference Pattern: See existing worker handler in worker/index.ts
Validation: Test with curl commands
Goal: Connect API handlers to worker
Files to Create/Modify:
worker/index.ts- Add routing for/api/contacts
Pseudocode Approach:
if (url.pathname.startsWith('/api/contacts')) {
return handleContactRequest(request, env);
}Validation: Routes respond correctly
Goal: Build user interface
Files to Create/Modify:
examples/database/d1-contact-form/ContactForm.tsx- Full component with form and list
Pseudocode Approach:
function ContactForm() {
// 1. Form state management
// 2. Validation logic
// 3. Submit handler (POST to API)
// 4. Fetch and display contacts
// 5. Delete handler
}Reference Pattern: React 19 hooks pattern (useState, useEffect)
Validation: Component renders without errors
Goal: Add component to app
Files to Create/Modify:
src/App.tsx- Import and render ContactForm
Validation: Form appears in browser
- Client-side errors: Display error messages in UI, prevent form submission
- Server-side errors: Return structured JSON errors with appropriate HTTP status codes
- Validation errors: Return 400 Bad Request with descriptive messages
- Network errors: Catch fetch errors and display user-friendly messages
- Database errors: Log to console, return generic error to client (don't expose internals)
-
Edge Case: Duplicate email submissions
- Solution: Allow duplicates (multiple submissions from same email are valid for contact forms)
-
Edge Case: Very long messages
- Solution: Enforce 5000 character limit on both client and server
-
Edge Case: SQL injection attempts
- Solution: Use parameterized queries exclusively (
.bind())
- Solution: Use parameterized queries exclusively (
-
Edge Case: Empty database (no contacts)
- Solution: Display "No contacts yet" message
-
Edge Case: Pagination beyond available records
- Solution: Return empty array with total count
-
Edge Case: Invalid contact ID (non-numeric, negative)
- Solution: Validate with parseInt, return 400 Bad Request
-
Edge Case: Database unavailable
- Solution: Catch errors, return 500 Internal Server Error
- Coverage Target: Core validation and type conversion functions
- Key Test Cases:
validateEmail()with valid/invalid emailsvalidateContactRequest()with various inputsrowToContact()date conversion
- Mock Strategy: Mock D1 database responses
- Test Scenarios:
- Complete flow: Create → List → Get → Delete
- Pagination with various limit/offset values
- Error responses for invalid inputs
- Setup Required: Local D1 database with test data
- Submit form with valid data
- Submit form with missing fields (should show validation errors)
- Submit form with invalid email (should show error)
- Submit form with very long message (should enforce limit)
- View list of contacts
- Delete a contact
- Test pagination with many contacts
- Verify SQL injection prevention (try malicious inputs)
- Test on both local and production environments
- Verify timestamps are correct
# Ensure Wrangler is installed
wrangler --version
# Verify D1 is available
wrangler d1 --help
# Check Node version
node --version # Should be 18+# Type checking (run after each step)
npm run build
# Generate types after D1 configuration
npm run cf-typegen
# Test worker locally
npm run dev# Build succeeds
npm run build
# Deploy to production
npm run deploy
# Test API endpoints
curl -X POST https://your-worker.workers.dev/api/contacts \
-H "Content-Type: application/json" \
-d '{"name":"Test","email":"test@example.com","message":"Test message"}'- Open browser to development URL
- Fill out and submit contact form
- Verify contact appears in list below
- Delete a contact
- Verify contact is removed from list
- Check browser console for errors
- Verify network requests in DevTools
None - uses existing dependencies (React, TypeScript, Cloudflare Workers runtime)
- Node: 18.x or higher
- Wrangler: 3.x or higher
- React: 19.x
- TypeScript: 5.x
-
Local Development:
npx wrangler d1 migrations apply contacts-db --local
-
Production:
npx wrangler d1 migrations apply contacts-db
Not applicable - this is an example feature, not a gradual rollout
- Create and configure D1 database
- Run migrations on local environment
- Test locally with
npm run dev - Run migrations on production database
- Deploy worker with
npm run deploy - Test production endpoints
- Monitor for errors
- All validation gates pass
- Contact form submits successfully
- Contacts appear in list
- Delete functionality works
- No TypeScript errors
- No SQL injection vulnerabilities
- Build succeeds
- API endpoints return proper JSON
- Error handling provides user-friendly messages
- Code follows existing conventions
- Documentation is complete
- Local and production databases work correctly
- No Authentication: Anyone can submit, view, and delete contacts (add auth for production)
- No Rate Limiting: Could be abused with spam (add rate limiting for production)
- No Email Notifications: Contacts are stored but not sent via email
- Basic Styling: Uses inline styles for simplicity (replace with CSS/Tailwind)
- No Search: Can't search contacts by name or email
- No Export: Can't export contacts to CSV/Excel
CLAUDE.md- Project guidelines and conventionsREADME.md- Project setup instructionswrangler.jsonc- Worker configuration
- Cloudflare D1 Documentation
- D1 Client API Reference
- Wrangler D1 Commands
- SQLite SQL Reference
- React 19 Documentation
// D1 Prepared Statement Pattern
const result = await env.DB.prepare(
'INSERT INTO table (column) VALUES (?)'
)
.bind(value)
.run();
// Getting last inserted ID
const lastId = result.meta.last_row_id;
// Fetching all results
const { results } = await env.DB.prepare(
'SELECT * FROM table'
).all();
// Fetching single result
const row = await env.DB.prepare(
'SELECT * FROM table WHERE id = ?'
)
.bind(id)
.first();- This example prioritizes simplicity and clarity over advanced features
- It demonstrates fundamental D1 patterns that can be extended for more complex use cases
- Consider adding authentication, rate limiting, and email notifications for production use
- The example uses inline styles for portability; replace with your preferred styling solution
{ "d1_databases": [ { "binding": "DB", "database_name": "contacts-db", "database_id": "xxxxx" } ] }