Deploy an Express.js application on Cloudflare Workers

Last updated Aug 25, 2026 In this tutorial, you will learn how to deploy an Express.js ↗︎ application on Cloudflare Workers using the Cloudflare Workers platform and D1 database. You will build a Members Registry API with basic Create, Read, Update, and Delete (CRUD) operations. You will use D1 as the database for storing and retrieving member data.

Before you start

All of the tutorials assume you have already completed the Get started guide, which gets you set up with a Cloudflare Workers account, C3 ↗︎, and Wrangler.

Quick start

If you want to skip the steps and get started quickly, select Deploy to Cloudflare below.

Deploy to Cloudflare

This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. Use this option if you are familiar with Cloudflare Workers, and wish to skip the step-by-step guidance.

You may wish to manually follow the steps if you are new to Cloudflare Workers.

1. Create a new Cloudflare Workers project

Use C3 ↗︎, the command-line tool for Cloudflare's developer products, to create a new directory and initialize a new Worker project:

npmyarnpnpm

npm create cloudflare@latest -- express-d1-app
yarn create cloudflare express-d1-app
pnpm create cloudflare@latest express-d1-app

For setup, select the following options:

  • For What would you like to start with?, choose Hello World example.
  • For Which template would you like to use?, choose Worker only.
  • For Which language do you want to use?, choose TypeScript.
  • For Do you want to use git for version control?, choose Yes.
  • For Do you want to add an AGENTS.md file to help AI coding tools understand Cloudflare APIs?, choose Yes.
  • For Do you want to deploy your application?, choose No (we will be making some changes before deploying).

Change into your new project directory:

cd express-d1-app

2. Install Express and dependencies

In this tutorial, you will use Express.js ↗︎, a popular web framework for Node.js. To use Express in a Cloudflare Workers environment, install Express along with the necessary TypeScript types:

npmyarnpnpmbun

npm i express @types/express
yarn add express @types/express
pnpm add express @types/express
bun add express @types/express

Express.js on Cloudflare Workers requires the nodejs_compat compatibility flag. This flag enables Node.js APIs and allows Express to run on the Workers runtime. Add the following to your Wrangler configuration file:

{
    "compatibility_flags": [
        "nodejs_compat"
    ]
}
compatibility_flags = [ "nodejs_compat" ]

3. Create a D1 database

You will now create a D1 database to store member information. Use the wrangler d1 create command to create a new database:

npx wrangler d1 create members-db

The command will create a new D1 database and ask you the following questions:

  • Would you like Wrangler to add it on your behalf?: Type Y.
  • What binding name would you like to use?: Type DB and press Enter.
  • For local dev, do you want to connect to the remote resource instead of a local resource?: Type N.
 ⛅️ wrangler 4.44.0
───────────────────
✅ Successfully created DB 'members-db' in region WNAM
Created your new D1 database.

To access your new D1 Database in your Worker, add the following snippet to your configuration file:
{
  "d1_databases": [
    {
      "binding": "members_db",
      "database_name": "members-db",
      "database_id": "<unique-ID-for-your-database>"
    }
  ]
}
✔ Would you like Wrangler to add it on your behalf? … yes
✔ What binding name would you like to use? … DB
✔ For local dev, do you want to connect to the remote resource instead of a local resource? … no

The binding will be added to your Wrangler configuration file.

{
    "d1_databases": [
        {
            "binding": "DB",
            "database_name": "members-db",
            "database_id": "<unique-ID-for-your-database>"
        }
    ]
}
[[d1_databases]]
binding = "DB"
database_name = "members-db"
database_id = "<unique-ID-for-your-database>"

4. Create database schema

Create a directory called schemas in your project root, and inside it, create a file called schema.sql:

schemas/schema.sqlsql

DROP TABLE IF EXISTS members;
CREATE TABLE IF NOT EXISTS members (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL,
  email TEXT NOT NULL UNIQUE,
  joined_date TEXT NOT NULL
);

-- Insert sample data
INSERT INTO members (name, email, joined_date) VALUES
  ('Alice Johnson', 'alice@example.com', '2024-01-15'),
  ('Bob Smith', 'bob@example.com', '2024-02-20'),
  ('Carol Williams', 'carol@example.com', '2024-03-10');

This schema creates a members table with an auto-incrementing ID, name, email, and join date fields. It also inserts three sample members.

Execute the schema file against your D1 database:

npx wrangler d1 execute members-db --file=./schemas/schema.sql

The above command creates the table in your local development database. You will deploy the schema to production later.

5. Initialize Express application

Update your src/index.ts file to set up Express with TypeScript. Replace the file content with the following:

src/index.tsts

import { env } from "cloudflare:workers";
import { httpServerHandler } from "cloudflare:node";
import express from "express";

const app = express();

// Middleware to parse JSON bodies
app.use(express.json());

// Health check endpoint
app.get("/", (req, res) => {
    res.json({ message: "Express.js running on Cloudflare Workers!" });
});

app.listen(3000);
export default httpServerHandler({ port: 3000 });

This code initializes Express and creates a basic health check endpoint. The key import import { env } from "cloudflare:workers" allows you to access bindings like your D1 database from anywhere in your code. The httpServerHandler integrates Express with the Workers runtime, enabling your application to handle HTTP requests on Cloudflare's network.

Next, execute the typegen command to generate type definitions for your Worker environment:

npm run cf-typegen

6. Implement read operations

Add endpoints to retrieve members from the database. Update your src/index.ts file by adding the following routes after the health check endpoint:

src/index.tsts

// GET all members
app.get('/api/members', async (req, res) => {
    try {
        const { results } = await env.DB.prepare('SELECT * FROM members ORDER BY joined_date DESC').all();

res.json({ success: true, members: results });
    } catch (error) {
        res.status(500).json({ success: false, error: 'Failed to fetch members' });
    }
});

// GET a single member by ID
app.get('/api/members/:id', async (req, res) => {
    try {
        const { id } = req.params;

const { results } = await env.DB.prepare('SELECT * FROM members WHERE id = ?').bind(id).all();

if (results.length === 0) {
            return res.status(404).json({ success: false, error: 'Member not found' });
        }

res.json({ success: true, member: results[0] });
    } catch (error) {
        res.status(500).json({ success: false, error: 'Failed to fetch member' });
    }
});

These routes use the D1 binding (env.DB) to prepare SQL statements and execute them. Since you imported env from cloudflare:workers at the top of the file, it is accessible throughout your application. The prepare, bind, and all methods on the D1 binding allow you to safely query the database. Refer to D1 Workers Binding API for all available methods.

7. Implement create operation

Add an endpoint to create new members. Add the following route to your src/index.ts file:

src/index.tsts

// POST - Create a new member
app.post("/api/members", async (req, res) => {
  try {
    const { name, email } = req.body;

// Validate input
    if (!name || !email) {
      return res.status(400).json({
        success: false,
        error: "Name and email are required",
      });
    }

// Basic email validation (simplified for tutorial purposes)
    // For production, consider using a validation library or more comprehensive checks
    if (!email.includes("@") || !email.includes(".")) {
      return res.status(400).json({
        success: false,
        error: "Invalid email format",
      });
    }

const joined_date = new Date().toISOString().split("T")[0];

const result = await env.DB.prepare(
      "INSERT INTO members (name, email, joined_date) VALUES (?, ?, ?)"
    )
      .bind(name, email, joined_date)
      .run();

if (result.success) {
      res.status(201).json({
        success: true,
        message: "Member created successfully",
        id: result.meta.last_row_id,
      });
    } else {
      res
        .status(500)
        .json({ success: false, error: "Failed to create member" });
    }
  } catch (error: any) {
    // Handle unique constraint violation
    if (error.message?.includes("UNIQUE constraint failed")) {
      return res.status(409).json({
        success: false,
        error: "Email already exists",
      });
    }
    res.status(500).json({ success: false, error: "Failed to create member" });
  }
});