Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions workers/express-on-workers/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "express-on-workers",
"version": "0.0.0",
"private": true,
"scripts": {
"deploy": "wrangler deploy",
"dev": "wrangler dev",
"start": "wrangler dev",
"test": "vitest",
"cf-typegen": "wrangler types"
},
"devDependencies": {
"@cloudflare/vitest-pool-workers": "^0.8.19",
"typescript": "^5.5.2",
"vitest": "~3.2.0",
"wrangler": "^4.44.0"
},
"dependencies": {
"@types/express": "^5.0.3",
"express": "^5.1.0"
}
}
13 changes: 13 additions & 0 deletions workers/express-on-workers/schemas/schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
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', '[email protected]', '2024-01-15'),
('Bob Smith', '[email protected]', '2024-02-20'),
('Carol Williams', '[email protected]', '2024-03-10');
110 changes: 110 additions & 0 deletions workers/express-on-workers/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
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!' });
});

// 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' });
}
});

// 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' });
}
});

// DELETE - Delete a member
app.delete('/api/members/:id', async (req, res) => {
try {
const { id } = req.params;

const result = await env.DB.prepare('DELETE FROM members WHERE id = ?').bind(id).run();

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

res.json({ success: true, message: 'Member deleted successfully' });
} catch (error) {
res.status(500).json({ success: false, error: 'Failed to delete member' });
}
});

app.listen(3000);
export default httpServerHandler({ port: 3000 });
3 changes: 3 additions & 0 deletions workers/express-on-workers/test/env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
declare module 'cloudflare:test' {
interface ProvidedEnv extends Env {}
}
24 changes: 24 additions & 0 deletions workers/express-on-workers/test/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { env, createExecutionContext, waitOnExecutionContext, SELF } from 'cloudflare:test';
import { describe, it, expect } from 'vitest';
import worker from '../src/index';

// For now, you'll need to do something like this to get a correctly-typed
// `Request` to pass to `worker.fetch()`.
const IncomingRequest = Request<unknown, IncomingRequestCfProperties>;

describe('Hello World worker', () => {
it('responds with Hello World! (unit style)', async () => {
const request = new IncomingRequest('http://example.com');
// Create an empty context to pass to `worker.fetch()`.
const ctx = createExecutionContext();
const response = await worker.fetch(request, env, ctx);
// Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions
await waitOnExecutionContext(ctx);
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
});

it('responds with Hello World! (integration style)', async () => {
const response = await SELF.fetch('https://example.com');
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
});
});
8 changes: 8 additions & 0 deletions workers/express-on-workers/test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"types": ["@cloudflare/vitest-pool-workers"]
},
"include": ["./**/*.ts", "../worker-configuration.d.ts"],
"exclude": []
}
45 changes: 45 additions & 0 deletions workers/express-on-workers/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */

/* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"target": "es2021",
/* Specify a set of bundled library declaration files that describe the target runtime environment. */
"lib": ["es2021"],
/* Specify what JSX code is generated. */
"jsx": "react-jsx",

/* Specify what module code is generated. */
"module": "es2022",
/* Specify how TypeScript looks up a file from a given module specifier. */
"moduleResolution": "Bundler",
/* Enable importing .json files */
"resolveJsonModule": true,

/* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
"allowJs": true,
/* Enable error reporting in type-checked JavaScript files. */
"checkJs": false,

/* Disable emitting files from a compilation. */
"noEmit": true,

/* Ensure that each file can be safely transpiled without relying on other imports. */
"isolatedModules": true,
/* Allow 'import x from y' when a module doesn't have a default export. */
"allowSyntheticDefaultImports": true,
/* Ensure that casing is correct in imports. */
"forceConsistentCasingInFileNames": true,

/* Enable all strict type-checking options. */
"strict": true,

/* Skip type checking all .d.ts files. */
"skipLibCheck": true,
"types": [
"./worker-configuration.d.ts"
]
},
"exclude": ["test"],
"include": ["worker-configuration.d.ts", "src/**/*.ts"]
}
11 changes: 11 additions & 0 deletions workers/express-on-workers/vitest.config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config';

export default defineWorkersConfig({
test: {
poolOptions: {
workers: {
wrangler: { configPath: './wrangler.jsonc' },
},
},
},
});
Loading