Skip to content

Commit 4aa8ec6

Browse files
authored
add express example code (#16)
1 parent 6bf6709 commit 4aa8ec6

File tree

10 files changed

+8686
-0
lines changed

10 files changed

+8686
-0
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "express-on-workers",
3+
"version": "0.0.0",
4+
"private": true,
5+
"scripts": {
6+
"deploy": "wrangler deploy",
7+
"dev": "wrangler dev",
8+
"start": "wrangler dev",
9+
"test": "vitest",
10+
"cf-typegen": "wrangler types"
11+
},
12+
"devDependencies": {
13+
"@cloudflare/vitest-pool-workers": "^0.8.19",
14+
"typescript": "^5.5.2",
15+
"vitest": "~3.2.0",
16+
"wrangler": "^4.44.0"
17+
},
18+
"dependencies": {
19+
"@types/express": "^5.0.3",
20+
"express": "^5.1.0"
21+
}
22+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
DROP TABLE IF EXISTS members;
2+
CREATE TABLE IF NOT EXISTS members (
3+
id INTEGER PRIMARY KEY AUTOINCREMENT,
4+
name TEXT NOT NULL,
5+
email TEXT NOT NULL UNIQUE,
6+
joined_date TEXT NOT NULL
7+
);
8+
9+
-- Insert sample data
10+
INSERT INTO members (name, email, joined_date) VALUES
11+
('Alice Johnson', '[email protected]', '2024-01-15'),
12+
('Bob Smith', '[email protected]', '2024-02-20'),
13+
('Carol Williams', '[email protected]', '2024-03-10');
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { env } from 'cloudflare:workers';
2+
import { httpServerHandler } from 'cloudflare:node';
3+
import express from 'express';
4+
5+
const app = express();
6+
7+
// Middleware to parse JSON bodies
8+
app.use(express.json());
9+
10+
// Health check endpoint
11+
app.get('/', (req, res) => {
12+
res.json({ message: 'Express.js running on Cloudflare Workers!' });
13+
});
14+
15+
// GET all members
16+
app.get('/api/members', async (req, res) => {
17+
try {
18+
const { results } = await env.DB.prepare('SELECT * FROM members ORDER BY joined_date DESC').all();
19+
20+
res.json({ success: true, members: results });
21+
} catch (error) {
22+
res.status(500).json({ success: false, error: 'Failed to fetch members' });
23+
}
24+
});
25+
26+
// GET a single member by ID
27+
app.get('/api/members/:id', async (req, res) => {
28+
try {
29+
const { id } = req.params;
30+
31+
const { results } = await env.DB.prepare('SELECT * FROM members WHERE id = ?').bind(id).all();
32+
33+
if (results.length === 0) {
34+
return res.status(404).json({ success: false, error: 'Member not found' });
35+
}
36+
37+
res.json({ success: true, member: results[0] });
38+
} catch (error) {
39+
res.status(500).json({ success: false, error: 'Failed to fetch member' });
40+
}
41+
});
42+
43+
// POST - Create a new member
44+
app.post('/api/members', async (req, res) => {
45+
try {
46+
const { name, email } = req.body;
47+
48+
// Validate input
49+
if (!name || !email) {
50+
return res.status(400).json({
51+
success: false,
52+
error: 'Name and email are required',
53+
});
54+
}
55+
56+
// Basic email validation (simplified for tutorial purposes)
57+
// For production, consider using a validation library or more comprehensive checks
58+
if (!email.includes('@') || !email.includes('.')) {
59+
return res.status(400).json({
60+
success: false,
61+
error: 'Invalid email format',
62+
});
63+
}
64+
65+
const joined_date = new Date().toISOString().split('T')[0];
66+
67+
const result = await env.DB.prepare('INSERT INTO members (name, email, joined_date) VALUES (?, ?, ?)')
68+
.bind(name, email, joined_date)
69+
.run();
70+
71+
if (result.success) {
72+
res.status(201).json({
73+
success: true,
74+
message: 'Member created successfully',
75+
id: result.meta.last_row_id,
76+
});
77+
} else {
78+
res.status(500).json({ success: false, error: 'Failed to create member' });
79+
}
80+
} catch (error: any) {
81+
// Handle unique constraint violation
82+
if (error.message?.includes('UNIQUE constraint failed')) {
83+
return res.status(409).json({
84+
success: false,
85+
error: 'Email already exists',
86+
});
87+
}
88+
res.status(500).json({ success: false, error: 'Failed to create member' });
89+
}
90+
});
91+
92+
// DELETE - Delete a member
93+
app.delete('/api/members/:id', async (req, res) => {
94+
try {
95+
const { id } = req.params;
96+
97+
const result = await env.DB.prepare('DELETE FROM members WHERE id = ?').bind(id).run();
98+
99+
if (result.meta.changes === 0) {
100+
return res.status(404).json({ success: false, error: 'Member not found' });
101+
}
102+
103+
res.json({ success: true, message: 'Member deleted successfully' });
104+
} catch (error) {
105+
res.status(500).json({ success: false, error: 'Failed to delete member' });
106+
}
107+
});
108+
109+
app.listen(3000);
110+
export default httpServerHandler({ port: 3000 });
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
declare module 'cloudflare:test' {
2+
interface ProvidedEnv extends Env {}
3+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { env, createExecutionContext, waitOnExecutionContext, SELF } from 'cloudflare:test';
2+
import { describe, it, expect } from 'vitest';
3+
import worker from '../src/index';
4+
5+
// For now, you'll need to do something like this to get a correctly-typed
6+
// `Request` to pass to `worker.fetch()`.
7+
const IncomingRequest = Request<unknown, IncomingRequestCfProperties>;
8+
9+
describe('Hello World worker', () => {
10+
it('responds with Hello World! (unit style)', async () => {
11+
const request = new IncomingRequest('http://example.com');
12+
// Create an empty context to pass to `worker.fetch()`.
13+
const ctx = createExecutionContext();
14+
const response = await worker.fetch(request, env, ctx);
15+
// Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions
16+
await waitOnExecutionContext(ctx);
17+
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
18+
});
19+
20+
it('responds with Hello World! (integration style)', async () => {
21+
const response = await SELF.fetch('https://example.com');
22+
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
23+
});
24+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"extends": "../tsconfig.json",
3+
"compilerOptions": {
4+
"types": ["@cloudflare/vitest-pool-workers"]
5+
},
6+
"include": ["./**/*.ts", "../worker-configuration.d.ts"],
7+
"exclude": []
8+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"compilerOptions": {
3+
/* Visit https://aka.ms/tsconfig.json to read more about this file */
4+
5+
/* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
6+
"target": "es2021",
7+
/* Specify a set of bundled library declaration files that describe the target runtime environment. */
8+
"lib": ["es2021"],
9+
/* Specify what JSX code is generated. */
10+
"jsx": "react-jsx",
11+
12+
/* Specify what module code is generated. */
13+
"module": "es2022",
14+
/* Specify how TypeScript looks up a file from a given module specifier. */
15+
"moduleResolution": "Bundler",
16+
/* Enable importing .json files */
17+
"resolveJsonModule": true,
18+
19+
/* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
20+
"allowJs": true,
21+
/* Enable error reporting in type-checked JavaScript files. */
22+
"checkJs": false,
23+
24+
/* Disable emitting files from a compilation. */
25+
"noEmit": true,
26+
27+
/* Ensure that each file can be safely transpiled without relying on other imports. */
28+
"isolatedModules": true,
29+
/* Allow 'import x from y' when a module doesn't have a default export. */
30+
"allowSyntheticDefaultImports": true,
31+
/* Ensure that casing is correct in imports. */
32+
"forceConsistentCasingInFileNames": true,
33+
34+
/* Enable all strict type-checking options. */
35+
"strict": true,
36+
37+
/* Skip type checking all .d.ts files. */
38+
"skipLibCheck": true,
39+
"types": [
40+
"./worker-configuration.d.ts"
41+
]
42+
},
43+
"exclude": ["test"],
44+
"include": ["worker-configuration.d.ts", "src/**/*.ts"]
45+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config';
2+
3+
export default defineWorkersConfig({
4+
test: {
5+
poolOptions: {
6+
workers: {
7+
wrangler: { configPath: './wrangler.jsonc' },
8+
},
9+
},
10+
},
11+
});

0 commit comments

Comments
 (0)