Building a Serverless CRUD API with Cloudflare Workers and D1 Database
Introduction
Today, I’ll show you how to build and deploy a serverless CRUD API using Cloudflare Workers and D1 — Cloudflare’s native SQLite database. We’ll create a simple user management system with authentication.
Prerequisites
- Node.js installed
- Cloudflare account
- Wrangler CLI (`npm install -g wrangler`)
Project Setup
First, authenticate with Cloudflare:
wrangler login
Create a new project:
wrangler init user-api
cd user-api
Setting Up D1 Database — Create a new D1 database:
wrangler d1 create users-db
Update wrangler.toml:
[[d1_databases]]
binding = "DB"
database_name = "users-db"
database_id = "<your-database-id>"
Create schema:
DROP TABLE IF EXISTS users;
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
Implementation
//index.ts
import { hash, compare } from 'bcryptjs';
interface Env {
DB: D1Database;
}
interface User {
id?: number;
name: string;
email: string;
password: string;
created_at?: string;
updated_at?: string;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
try {
const url = new URL(request.url);
const path = url.pathname;
// GET all users (password excluded)
if (request.method === 'GET' && path === '/api/users') {
const { results } = await env.DB.prepare(
'SELECT id, name, email, created_at, updated_at FROM users'
).all();
return Response.json(results);
}
// GET single user
if (request.method === 'GET' && path.match(/^\/api\/users\/\d+$/)) {
const id = path.split('/')[3];
const { results } = await env.DB.prepare(
'SELECT id, name, email, created_at, updated_at FROM users WHERE id = ?'
).bind(id).all();
if (results.length === 0) {
return new Response('User not found', { status: 404 });
}
return Response.json(results[0]);
}
// POST create user
if (request.method === 'POST' && path === '/api/users') {
const user: User = await request.json();
if (!user.name || !user.email || !user.password) {
return new Response('Name, email and password are required', { status: 400 });
}
// Hash password
const hashedPassword = await hash(user.password, 10);
const { success } = await env.DB.prepare(
'INSERT INTO users (name, email, password) VALUES (?, ?, ?)'
)
.bind(user.name, user.email, hashedPassword)
.run();
if (!success) {
return new Response('Failed to create user', { status: 500 });
}
return new Response('User created successfully', { status: 201 });
}
// PUT update user
if (request.method === 'PUT' && path.match(/^\/api\/users\/\d+$/)) {
const id = path.split('/')[3];
const user: User = await request.json();
const updates = [];
const bindings = [];
if (user.name) {
updates.push('name = ?');
bindings.push(user.name);
}
if (user.email) {
updates.push('email = ?');
bindings.push(user.email);
}
if (user.password) {
updates.push('password = ?');
bindings.push(await hash(user.password, 10));
}
updates.push('updated_at = CURRENT_TIMESTAMP');
bindings.push(id);
const { success } = await env.DB.prepare(
`UPDATE users SET ${updates.join(', ')} WHERE id = ?`
)
.bind(...bindings)
.run();
if (!success) {
return new Response('Failed to update user', { status: 500 });
}
return new Response('User updated successfully', { status: 200 });
}
// DELETE user
if (request.method === 'DELETE' && path.match(/^\/api\/users\/\d+$/)) {
const id = path.split('/')[3];
const { success } = await env.DB.prepare(
'DELETE FROM users WHERE id = ?'
)
.bind(id)
.run();
if (!success) {
return new Response('Failed to delete user', { status: 500 });
}
return new Response('User deleted successfully', { status: 200 });
}
return new Response('Not found', { status: 404 });
} catch (error) {
if (error instanceof Error) {
return new Response(`Error: ${error.message}`, { status: 500 });
} else {
return new Response('Unknown error', { status: 500 });
}
}
},
};
API Endpoints
Our API provides these endpoints:
- GET `/api/users` — List all users
- GET `/api/users/:id` — Get single user
- POST `/api/users` — Create user
- PUT `/api/users/:id` — Update user
- DELETE `/api/users/:id` — Delete user
Deployment
Deploy to Cloudflare’s network:
wrangler deploy
Testing
Test with curl:
# Create user
curl -X POST https://<your-worker>.workers.dev/api/users \
-H "Content-Type: application/json" \
-d '{"name":"John","email":"john@example.com","password":"secret"}'
# List users
curl https://<your-worker>.workers.dev/api/users
Key Features
- Serverless architecture
- Global distribution via Cloudflare’s network
- SQL database with D1
- Password hashing
- Error handling
- TypeScript support
Benefits
1. Zero infrastructure management
2. Automatic scaling
3. Low latency via edge deployment
4. Built-in security features
5. Cost-effective (generous free tier)
Conclusion
Cloudflare Workers with D1 provides a powerful platform for building serverless APIs. The combination offers excellent performance, global distribution, and a great developer experience.
Full code available on GitHub: Cloudflare-CRUD-API
#CloudflareWorkers #Serverless #WebDevelopment #TypeScript #Tutorial