A simple serverless GraphQL API for managing todos, built with Cloudflare Workers, D1 database, and GraphQL Yoga.
- ✅ Full CRUD operations for todos
- 🗄️ Persistent storage with Cloudflare D1 (SQLite)
- 🚀 Serverless deployment on Cloudflare's edge network
- 📝 Strong TypeScript typing with comprehensive validation
- 🎯 GraphQL with interactive playground
- 🔒 Security hardened with input validation (D1 prepared statement methods) and CORS
- 📊 Structured logging for observability
- 🛡️ Error handling
npm install# Create the database
npx wrangler d1 create todo-dbCopy the database_id from the output and update it in wrangler.jsonc:
# For local development
npx wrangler d1 execute todo-db --local --file=./schema.sql
# For production
npx wrangler d1 execute todo-db --remote --file=./schema.sqlThe schema creates a todos table with automatic timestamps and indexes. See schema.sql for details.
npm run devVisit http://localhost:8787/graphql to access the GraphQL Playground where you can test queries and mutations interactively.
npm run deployYour API will be available at https://your-worker-name.your-subdomain.workers.dev/graphql
type Todo {
id: Int!
title: String!
completed: Boolean!
createdAt: String!
}query GetAllTodos {
todos {
id
title
completed
createdAt
}
}Response Example:
{
"data": {
"todos": [
{
"id": 1,
"title": "Deploy to production",
"completed": false,
"createdAt": "2025-11-10T10:30:00.000Z"
},
{
"id": 2,
"title": "Set up monitoring",
"completed": true,
"createdAt": "2025-11-09T15:20:00.000Z"
}
]
}
}query GetTodo {
todo(id: 1) {
id
title
completed
createdAt
}
}Returns null if todo doesn't exist.
mutation CreateTodo {
createTodo(title: "Buy groceries") {
id
title
completed
createdAt
}
}Validation:
- Title is required (1-500 characters)
- Whitespace is automatically trimmed
- New todos start with
completed: false
# Update title only
mutation UpdateTitle {
updateTodo(id: 1, title: "Buy groceries and cook dinner") {
id
title
completed
createdAt
}
}
# Update completion status only
mutation MarkComplete {
updateTodo(id: 1, completed: true) {
id
title
completed
createdAt
}
}
# Update both fields
mutation UpdateBoth {
updateTodo(id: 1, title: "Updated task", completed: true) {
id
title
completed
createdAt
}
}Validation:
- At least one field (title or completed) must be provided
- Returns
nullif todo doesn't exist
mutation DeleteTodo {
deleteTodo(id: 1)
}Returns true if deleted successfully, false if todo doesn't exist.
The API provides clear error messages for common issues:
Invalid Input:
mutation {
createTodo(title: "")
}
# Error: "Title must be at least 1 character(s)"Invalid ID:
query {
todo(id: -1)
}
# Error: "ID must be a positive integer"Title Too Long:
mutation {
createTodo(title: "Very long title exceeding 500 characters...")
}
# Error: "Title must not exceed 500 characters".
├── src/
│ └── index.ts # Main Worker with GraphQL resolvers
├── public/ # Static assets (optional)
├── schema.sql # D1 database schema
├── wrangler.jsonc # Cloudflare Workers configuration
├── tsconfig.json # TypeScript configuration
├── package.json # Dependencies and scripts
└── README.md # This file
- Input Validation: Title length limits (1-500 chars), type checking
- SQL Injection Prevention: Prepared statements with parameter binding
- Input Sanitization: Automatic whitespace trimming
- Security Headers:
X-Content-Type-Options,X-Frame-Options,X-XSS-Protection - CORS Configuration: Configurable cross-origin access
- Request Tracking: Unique request IDs for audit trails
This API is protected by Cloudflare API Shield with the following limits:
- Maximum Query Size: 10 fields
- Maximum Query Depth: 2 levels
Based on GraphQL schema: Query Size Limit: 10-15 fields, Query Depth Limit: 2-3 levels.
Example Cloudflare WAF Custom Rules expression:
(http.request.uri.path eq "/graphql" and cf.api_gateway.graphql.query_size > 10 and cf.api_gateway.graphql.parsed_successfully) or (http.request.uri.path eq "/graphql" and cf.api_gateway.graphql.query_depth > 2 and cf.api_gateway.graphql.parsed_successfully)
These limits prevent malicious queries that could overload the database while allowing all legitimate operations.
All standard operations work normally:
- ✅ Safe - Get all todos (size: 4, depth: 1)
- ✅ Safe - Get single todo (size: 4, depth: 1)
- ✅ Safe - Multiple queries (size: 8, depth: 1)
- ❌ Blocked - Queries exceeding 10 fields or depth > 2 (see example queries here)
Before deploying to production, update src/index.ts:
-
Restrict CORS origin (line ~280):
const CORS_HEADERS = { 'Access-Control-Allow-Origin': 'https://yourdomain.com', // Change from '*' // ... };
-
Enable masked errors:
const yoga = createYoga({ // ... maskedErrors: true, // Uncomment this line });
-
Add rate limiting and configure additional security rules.
-
Configure graphql-yoga properly:
const yoga = createYoga({ schema, graphqlEndpoint: '/graphql', landingPage: false, // Disable in production graphiql: false, // Disable GraphiQL // Disable introspection in production disableIntrospection: true, });
All operations are logged in JSON format with:
- Timestamps
- Request IDs for tracing
- Operation durations
- Success/failure status
- Error details
View logs in real-time:
npx wrangler tail{
"timestamp": "2025-11-10T10:30:00.000Z",
"level": "info",
"message": "Created todo successfully",
"id": 5,
"durationMs": 45
}- Analytics: View request volume and errors in Cloudflare Dashboard
- Logpush: Export logs to external services (S3, R2, etc.)
- Workers Analytics Engine: Custom metrics and analytics
Use the GraphQL Playground at http://localhost:8787/graphql or use curl:
# Query example
curl -X POST http://localhost:8787/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ todos { id title } }"}'
# Mutation example
curl -X POST http://localhost:8787/graphql \
-H "Content-Type: application/json" \
-d '{"query": "mutation { createTodo(title: \"Test\") { id } }"}'# Execute queries against production database
npx wrangler d1 execute todo-db --remote --command "SELECT * FROM todos"
# Run SQL file against production
npx wrangler d1 execute todo-db --remote --file=./migration.sql- Cloudflare Workers - Serverless platform
- Cloudflare D1 - SQL database
- GraphQL Yoga - GraphQL server
- Wrangler CLI - Development tool
- API Shield GraphQL malicious query protection – Recommended API security feature
- GraphQL Best Practices
- D1 Pricing - Free tier available
- Workers Limits
- GraphQL Yoga on Cloudflare Workers
- Edge Serverless GraphQL
- Building a GraphQL server on the edge with Cloudflare Workers
- Cloudflare GraphQL Server Examples
- GraphQL and Serverless Architecture
- Building a GraphQL Yoga Server with TypeScript on Cloudflare Workers with Cloudflare KV
This is a demonstration project showcasing Cloudflare Workers and D1 database capabilities with a minimal demonstration of security features and best practices. While it implements comprehensive security measures, additional considerations (authentication, authorization, advanced monitoring) should be evaluated based on your specific production requirements.
Educational and demonstration purposes only.