|
| 1 | +--- |
| 2 | +reviewed: 2025-10-21 |
| 3 | +difficulty: Beginner |
| 4 | +pcx_content_type: tutorial |
| 5 | +title: Deploy an Express.js application on Cloudflare Workers |
| 6 | +products: [workers, d1] |
| 7 | +tags: |
| 8 | + - TypeScript |
| 9 | +description: >- |
| 10 | + Learn how to deploy an Express.js application on Cloudflare Workers. |
| 11 | +--- |
| 12 | + |
| 13 | +import { |
| 14 | + Render, |
| 15 | + WranglerConfig, |
| 16 | + PackageManagers, |
| 17 | + GitHubCode, |
| 18 | +} from "~/components"; |
| 19 | + |
| 20 | +In this tutorial, you will learn how to deploy an [Express.js](https://expressjs.com/) application on Cloudflare Workers using the [Cloudflare Workers platform](/workers/) and [D1 database](/d1/). 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. |
| 21 | + |
| 22 | +<Render file="tutorials-before-you-start" product="workers" /> |
| 23 | + |
| 24 | +## Quick start |
| 25 | + |
| 26 | +If you want to skip the steps and get started quickly, select **Deploy to Cloudflare** below. |
| 27 | + |
| 28 | +[](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/express-on-workers) |
| 29 | + |
| 30 | +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. |
| 31 | + |
| 32 | +You may wish to manually follow the steps if you are new to Cloudflare Workers. |
| 33 | + |
| 34 | +## 1. Create a new Cloudflare Workers project |
| 35 | + |
| 36 | +Use [C3](https://developers.cloudflare.com/learning-paths/workers/get-started/c3-and-wrangler/#c3), the command-line tool for Cloudflare's developer products, to create a new directory and initialize a new Worker project: |
| 37 | + |
| 38 | +<PackageManagers |
| 39 | + type="create" |
| 40 | + pkg="cloudflare@latest" |
| 41 | + args={"express-d1-app"} |
| 42 | +/> |
| 43 | + |
| 44 | +<Render |
| 45 | + file="c3-post-run-steps" |
| 46 | + product="workers" |
| 47 | + params={{ |
| 48 | + category: "hello-world", |
| 49 | + type: "Worker only", |
| 50 | + lang: "TypeScript", |
| 51 | + }} |
| 52 | +/> |
| 53 | + |
| 54 | +Change into your new project directory: |
| 55 | + |
| 56 | +```sh frame="none" |
| 57 | +cd express-d1-app |
| 58 | +``` |
| 59 | + |
| 60 | +## 2. Install Express and dependencies |
| 61 | + |
| 62 | +In this tutorial, you will use [Express.js](https://expressjs.com/), a popular web framework for Node.js. To use Express in a Cloudflare Workers environment, install Express along with the necessary TypeScript types: |
| 63 | + |
| 64 | +<PackageManagers type="add" pkg="express @types/express" /> |
| 65 | + |
| 66 | +Express.js on Cloudflare Workers requires the `nodejs_compat` [compatibility flag](/workers/configuration/compatibility-flags/). This flag enables Node.js APIs and allows Express to run on the Workers runtime. Add the following to your `wrangler.toml` file: |
| 67 | + |
| 68 | +<WranglerConfig> |
| 69 | + |
| 70 | +```toml |
| 71 | +compatibility_flags = ["nodejs_compat"] |
| 72 | +``` |
| 73 | + |
| 74 | +</WranglerConfig> |
| 75 | + |
| 76 | +## 3. Create a D1 database |
| 77 | + |
| 78 | +You will now create a D1 database to store member information. Use the `wrangler d1 create` command to create a new database: |
| 79 | + |
| 80 | +```sh frame="none" |
| 81 | +npx wrangler d1 create members-db |
| 82 | +``` |
| 83 | + |
| 84 | +The command will create a new D1 database and ask you the following questions: |
| 85 | + |
| 86 | +- **Would you like Wrangler to add it on your behalf?**: Type `Y`. |
| 87 | +- **What binding name would you like to use?**: Type `DB` and press Enter. |
| 88 | +- **For local dev, do you want to connect to the remote resource instead of a local resource?**: Type `N`. |
| 89 | + |
| 90 | +```sh output |
| 91 | + ⛅️ wrangler 4.44.0 |
| 92 | +─────────────────── |
| 93 | +✅ Successfully created DB 'members-db' in region WNAM |
| 94 | +Created your new D1 database. |
| 95 | + |
| 96 | +To access your new D1 Database in your Worker, add the following snippet to your configuration file: |
| 97 | +{ |
| 98 | + "d1_databases": [ |
| 99 | + { |
| 100 | + "binding": "members_db", |
| 101 | + "database_name": "members-db", |
| 102 | + "database_id": "<unique-ID-for-your-database>" |
| 103 | + } |
| 104 | + ] |
| 105 | +} |
| 106 | +✔ Would you like Wrangler to add it on your behalf? … yes |
| 107 | +✔ What binding name would you like to use? … DB |
| 108 | +✔ For local dev, do you want to connect to the remote resource instead of a local resource? … no |
| 109 | +``` |
| 110 | + |
| 111 | +The binding will be added to your wrangler configuration file. |
| 112 | + |
| 113 | +<WranglerConfig> |
| 114 | + |
| 115 | +```toml |
| 116 | +[[d1_databases]] |
| 117 | +binding = "DB" |
| 118 | +database_name = "members-db" |
| 119 | +database_id = "<unique-ID-for-your-database>" |
| 120 | +``` |
| 121 | + |
| 122 | +</WranglerConfig> |
| 123 | + |
| 124 | +## 4. Create database schema |
| 125 | + |
| 126 | +Create a directory called `schemas` in your project root, and inside it, create a file called `schema.sql`: |
| 127 | + |
| 128 | +<GitHubCode |
| 129 | + repo="cloudflare/docs-examples" |
| 130 | + file="workers/express-on-workers/schemas/schema.sql" |
| 131 | + commit="4aa8ec65004ab31112a326524a530e98af872787" |
| 132 | + lines="1-13" |
| 133 | + lang="sql" |
| 134 | + code={{ |
| 135 | + title: "schemas/schema.sql", |
| 136 | + }} |
| 137 | +/> |
| 138 | + |
| 139 | +This schema creates a `members` table with an auto-incrementing ID, name, email, and join date fields. It also inserts three sample members. |
| 140 | + |
| 141 | +Execute the schema file against your D1 database: |
| 142 | + |
| 143 | +```sh frame="none" |
| 144 | +npx wrangler d1 execute members-db --file=./schemas/schema.sql |
| 145 | +``` |
| 146 | + |
| 147 | +The above command creates the table in your local development database. You will deploy the schema to production later. |
| 148 | + |
| 149 | +## 5. Initialize Express application |
| 150 | + |
| 151 | +Update your `src/index.ts` file to set up Express with TypeScript. Replace the file content with the following: |
| 152 | + |
| 153 | +```ts title="src/index.ts" |
| 154 | +import { env } from "cloudflare:workers"; |
| 155 | +import { httpServerHandler } from "cloudflare:node"; |
| 156 | +import express from "express"; |
| 157 | + |
| 158 | +const app = express(); |
| 159 | + |
| 160 | +// Middleware to parse JSON bodies |
| 161 | +app.use(express.json()); |
| 162 | + |
| 163 | +// Health check endpoint |
| 164 | +app.get("/", (req, res) => { |
| 165 | + res.json({ message: "Express.js running on Cloudflare Workers!" }); |
| 166 | +}); |
| 167 | + |
| 168 | +app.listen(3000); |
| 169 | +export default httpServerHandler({ port: 3000 }); |
| 170 | +``` |
| 171 | + |
| 172 | +This code initializes Express and creates a basic health check endpoint. The key import `import { env } from "cloudflare:workers"` allows you to access [bindings](/workers/runtime-apis/bindings/) like your D1 database from anywhere in your code. The [httpServerHandler](/workers/runtime-apis/nodejs/http/#httpserverhandler) integrates Express with the Workers runtime, enabling your application to handle HTTP requests on Cloudflare's network. |
| 173 | + |
| 174 | +Next, execute the typegen command to generate type definitions for your Worker environment: |
| 175 | + |
| 176 | +```sh frame="none" |
| 177 | +npm run cf-typegen |
| 178 | +``` |
| 179 | + |
| 180 | +## 6. Implement read operations |
| 181 | + |
| 182 | +Add endpoints to retrieve members from the database. Update your `src/index.ts` file by adding the following routes after the health check endpoint: |
| 183 | + |
| 184 | +<GitHubCode |
| 185 | + repo="cloudflare/docs-examples" |
| 186 | + file="workers/express-on-workers/src/index.ts" |
| 187 | + commit="4aa8ec65004ab31112a326524a530e98af872787" |
| 188 | + lines="15-41" |
| 189 | + lang="typescript" |
| 190 | + code={{ |
| 191 | + title: "src/index.ts", |
| 192 | + }} |
| 193 | +/> |
| 194 | + |
| 195 | +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](/d1/worker-api/) for all available methods. |
| 196 | + |
| 197 | +## 7. Implement create operation |
| 198 | + |
| 199 | +Add an endpoint to create new members. Add the following route to your `src/index.ts` file: |
| 200 | + |
| 201 | +<GitHubCode |
| 202 | + repo="cloudflare/docs-examples" |
| 203 | + file="workers/express-on-workers/src/index.ts" |
| 204 | + commit="f223061b9d1717905154980c200bf82263b43aee" |
| 205 | + lines="113-164" |
| 206 | + lang="typescript" |
| 207 | + code={{ |
| 208 | + title: "src/index.ts", |
| 209 | + }} |
| 210 | +/> |
| 211 | + |
| 212 | +This endpoint validates the input, checks the email format, and inserts a new member into the database. It also handles duplicate email addresses by checking for unique constraint violations. |
| 213 | + |
| 214 | +## 8. Implement update operation |
| 215 | + |
| 216 | +Add an endpoint to update existing members. Add the following route to your `src/index.ts` file: |
| 217 | + |
| 218 | +<GitHubCode |
| 219 | + repo="cloudflare/docs-examples" |
| 220 | + file="workers/express-on-workers/src/index.ts" |
| 221 | + commit="f223061b9d1717905154980c200bf82263b43aee" |
| 222 | + lines="52-111" |
| 223 | + lang="typescript" |
| 224 | + code={{ |
| 225 | + title: "src/index.ts", |
| 226 | + }} |
| 227 | +/> |
| 228 | + |
| 229 | +This endpoint allows updating either the name, email, or both fields of an existing member. It builds a dynamic SQL query based on the provided fields. |
| 230 | + |
| 231 | +## 9. Implement delete operation |
| 232 | + |
| 233 | +Add an endpoint to delete members. Add the following route to your `src/index.ts` file: |
| 234 | + |
| 235 | +<GitHubCode |
| 236 | + repo="cloudflare/docs-examples" |
| 237 | + file="workers/express-on-workers/src/index.ts" |
| 238 | + commit="f223061b9d1717905154980c200bf82263b43aee" |
| 239 | + lines="166-185" |
| 240 | + lang="typescript" |
| 241 | + code={{ |
| 242 | + title: "src/index.ts", |
| 243 | + }} |
| 244 | +/> |
| 245 | + |
| 246 | +This endpoint deletes a member by their ID and returns an error if the member does not exist. |
| 247 | + |
| 248 | +## 10. Test locally |
| 249 | + |
| 250 | +Start the development server to test your API locally: |
| 251 | + |
| 252 | +```sh frame="none" |
| 253 | +npm run dev |
| 254 | +``` |
| 255 | + |
| 256 | +The development server will start, and you can access your API at `http://localhost:8787`. |
| 257 | + |
| 258 | +Open a new terminal window and test the endpoints using `curl`: |
| 259 | + |
| 260 | +```sh title="Get all members" |
| 261 | +curl http://localhost:8787/api/members |
| 262 | +``` |
| 263 | + |
| 264 | +```json output |
| 265 | +{ |
| 266 | + "success": true, |
| 267 | + "members": [ |
| 268 | + { |
| 269 | + "id": 1, |
| 270 | + "name": "Alice Johnson", |
| 271 | + |
| 272 | + "joined_date": "2024-01-15" |
| 273 | + }, |
| 274 | + { |
| 275 | + "id": 2, |
| 276 | + "name": "Bob Smith", |
| 277 | + |
| 278 | + "joined_date": "2024-02-20" |
| 279 | + }, |
| 280 | + { |
| 281 | + "id": 3, |
| 282 | + "name": "Carol Williams", |
| 283 | + |
| 284 | + "joined_date": "2024-03-10" |
| 285 | + } |
| 286 | + ] |
| 287 | +} |
| 288 | +``` |
| 289 | + |
| 290 | +Test creating a new member: |
| 291 | + |
| 292 | +```sh title="Create a member" |
| 293 | +curl -X POST http://localhost:8787/api/members \ |
| 294 | + -H "Content-Type: application/json" \ |
| 295 | + -d '{"name": "David Brown", "email": "[email protected]"}' |
| 296 | +``` |
| 297 | + |
| 298 | +```json output |
| 299 | +{ |
| 300 | + "success": true, |
| 301 | + "message": "Member created successfully", |
| 302 | + "id": 4 |
| 303 | +} |
| 304 | +``` |
| 305 | + |
| 306 | +Test getting a single member: |
| 307 | + |
| 308 | +```sh title="Get a member by ID" |
| 309 | +curl http://localhost:8787/api/members/1 |
| 310 | +``` |
| 311 | + |
| 312 | +Test updating a member: |
| 313 | + |
| 314 | +```sh title="Update a member" |
| 315 | +curl -X PUT http://localhost:8787/api/members/1 \ |
| 316 | + -H "Content-Type: application/json" \ |
| 317 | + -d '{"name": "Alice Cooper"}' |
| 318 | +``` |
| 319 | + |
| 320 | +Test deleting a member: |
| 321 | + |
| 322 | +```sh title="Delete a member" |
| 323 | +curl -X DELETE http://localhost:8787/api/members/4 |
| 324 | +``` |
| 325 | + |
| 326 | +## 11. Deploy to Cloudflare Workers |
| 327 | + |
| 328 | +Before deploying to production, execute the schema file against your remote (production) database: |
| 329 | + |
| 330 | +```sh frame="none" |
| 331 | +npx wrangler d1 execute members-db --remote --file=./schemas/schema.sql |
| 332 | +``` |
| 333 | + |
| 334 | +Now deploy your application to the Cloudflare network: |
| 335 | + |
| 336 | +```sh frame="none" |
| 337 | +npm run deploy |
| 338 | +``` |
| 339 | + |
| 340 | +```sh output |
| 341 | +⛅️ wrangler 4.44.0 |
| 342 | +─────────────────── |
| 343 | +Total Upload: 1743.64 KiB / gzip: 498.65 KiB |
| 344 | +Worker Startup Time: 48 ms |
| 345 | +Your Worker has access to the following bindings: |
| 346 | +Binding Resource |
| 347 | +env.DB (members-db) D1 Database |
| 348 | + |
| 349 | +Uploaded express-d1-app (2.99 sec) |
| 350 | +Deployed express-d1-app triggers (5.26 sec) |
| 351 | + https://<your-subdomain>.workers.dev |
| 352 | +Current Version ID: <version-id> |
| 353 | +``` |
| 354 | + |
| 355 | +After successful deployment, Wrangler will output your Worker's URL. |
| 356 | + |
| 357 | +## 12. Test production deployment |
| 358 | + |
| 359 | +Test your deployed API using the provided URL. Replace `<your-worker-url>` with your actual Worker URL: |
| 360 | + |
| 361 | +```sh title="Test production API" |
| 362 | +curl https://<your-worker-url>/api/members |
| 363 | +``` |
| 364 | + |
| 365 | +You should see the same member data you created in the production database. |
| 366 | + |
| 367 | +Create a new member in production: |
| 368 | + |
| 369 | +```sh title="Create a member in production" |
| 370 | +curl -X POST https://<your-worker-url>/api/members \ |
| 371 | + -H "Content-Type: application/json" \ |
| 372 | + -d '{"name": "Eva Martinez", "email": "[email protected]"}' |
| 373 | +``` |
| 374 | + |
| 375 | +Your Express.js application with D1 database is now running on Cloudflare Workers. |
| 376 | + |
| 377 | +## Conclusion |
| 378 | + |
| 379 | +In this tutorial, you built a Members Registry API using Express.js and D1 database, then deployed it to Cloudflare Workers. You implemented full CRUD operations (Create, Read, Update, Delete) and learned how to: |
| 380 | + |
| 381 | +- Set up an Express.js application for Cloudflare Workers |
| 382 | +- Create and configure a D1 database with bindings |
| 383 | +- Implement database operations using D1's prepared statements |
| 384 | +- Test your API locally and in production |
| 385 | + |
| 386 | +## Next steps |
| 387 | + |
| 388 | +- Learn more about [D1 database features](/d1/) |
| 389 | +- Explore [Workers routing and middleware](/workers/runtime-apis/) |
| 390 | +- Add authentication to your API using [Workers authentication](/workers/runtime-apis/handlers/) |
| 391 | +- Implement pagination for large datasets using [D1 query optimization](/d1/worker-api/) |
0 commit comments