diff --git a/frameworks/express/Dockerfile b/frameworks/express/Dockerfile new file mode 100644 index 0000000..aed2515 --- /dev/null +++ b/frameworks/express/Dockerfile @@ -0,0 +1,9 @@ +FROM node:22-slim +RUN apt-get update && apt-get install -y --no-install-recommends python3 make g++ && rm -rf /var/lib/apt/lists/* +WORKDIR /app +COPY package.json . +RUN npm install --omit=dev +COPY server.js . +ENV NODE_ENV=production +EXPOSE 8080 +CMD ["node", "server.js"] diff --git a/frameworks/express/README.md b/frameworks/express/README.md new file mode 100644 index 0000000..5638796 --- /dev/null +++ b/frameworks/express/README.md @@ -0,0 +1,20 @@ +# Express + +[Express](https://github.com/expressjs/express) (~69k ⭐) — the most widely used backend framework in the JavaScript ecosystem. Fast, unopinionated, minimalist web framework for Node.js. + +## Setup + +- **Express 5.x** with Node.js 22 +- **Cluster mode** — one worker per CPU core +- **better-sqlite3** for `/db` endpoint (mmap, read-only) +- **HTTP/2** on port 8443 (native `http2` module, raw handler for performance) +- **Pre-computed gzip** for `/compression` + +## Why Express? + +HttpArena already has bare `node`, `fastify`, `ultimate-express`, `hono`, and `bun` — but not vanilla Express itself. Express is the framework that started it all for Node.js web development. + +The key comparisons: +- **Express vs Fastify**: The classic Node.js framework showdown — Express's flexibility vs Fastify's schema-based speed +- **Express vs ultimate-express**: How does the original compare to its high-performance reimplementation? +- **Express vs bare node**: How much overhead does the Express middleware layer actually add? diff --git a/frameworks/express/meta.json b/frameworks/express/meta.json new file mode 100644 index 0000000..2258e13 --- /dev/null +++ b/frameworks/express/meta.json @@ -0,0 +1,21 @@ +{ + "display_name": "Express", + "language": "JS", + "type": "framework", + "engine": "V8", + "description": "Fast, unopinionated, minimalist web framework for Node.js. The most widely used backend framework in the JavaScript ecosystem.", + "repo": "https://github.com/expressjs/express", + "enabled": true, + "tests": [ + "baseline", + "pipelined", + "limited-conn", + "json", + "upload", + "compression", + "mixed", + "noisy", + "baseline-h2", + "static-h2" + ] +} diff --git a/frameworks/express/package.json b/frameworks/express/package.json new file mode 100644 index 0000000..d72bfca --- /dev/null +++ b/frameworks/express/package.json @@ -0,0 +1,9 @@ +{ + "name": "httparena-express", + "version": "1.0.0", + "private": true, + "dependencies": { + "express": "^5.1.0", + "better-sqlite3": "^11.0.0" + } +} diff --git a/frameworks/express/server.js b/frameworks/express/server.js new file mode 100644 index 0000000..0e076ad --- /dev/null +++ b/frameworks/express/server.js @@ -0,0 +1,233 @@ +const cluster = require('cluster'); +const os = require('os'); +const fs = require('fs'); +const http = require('http'); +const http2 = require('http2'); +const zlib = require('zlib'); + +const SERVER_NAME = 'express'; + +// --- Shared data (loaded per-worker) --- +let datasetItems; +let largeJsonBuf; +let dbStmt; +const staticFiles = {}; +const MIME_TYPES = { + '.css': 'text/css', '.js': 'application/javascript', '.html': 'text/html', + '.woff2': 'font/woff2', '.svg': 'image/svg+xml', '.webp': 'image/webp', '.json': 'application/json' +}; + +function loadStaticFiles() { + const dir = '/data/static'; + try { + for (const name of fs.readdirSync(dir)) { + const buf = fs.readFileSync(dir + '/' + name); + const ext = name.slice(name.lastIndexOf('.')); + staticFiles[name] = { buf, ct: MIME_TYPES[ext] || 'application/octet-stream' }; + } + } catch (e) {} +} + +function loadDataset() { + const path = process.env.DATASET_PATH || '/data/dataset.json'; + try { + datasetItems = JSON.parse(fs.readFileSync(path, 'utf8')); + } catch (e) {} +} + +function loadLargeDataset() { + try { + const raw = JSON.parse(fs.readFileSync('/data/dataset-large.json', 'utf8')); + const items = raw.map(d => ({ + id: d.id, name: d.name, category: d.category, + price: d.price, quantity: d.quantity, active: d.active, + tags: d.tags, rating: d.rating, + total: Math.round(d.price * d.quantity * 100) / 100 + })); + largeJsonBuf = Buffer.from(JSON.stringify({ items, count: items.length })); + } catch (e) {} +} + +function loadDatabase() { + try { + const Database = require('better-sqlite3'); + const db = new Database('/data/benchmark.db', { readonly: true }); + db.pragma('mmap_size=268435456'); + dbStmt = db.prepare('SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count FROM items WHERE price BETWEEN ? AND ? LIMIT 50'); + } catch (e) {} +} + +function sumQuery(query) { + let sum = 0; + if (query) { + for (const key of Object.keys(query)) { + const n = parseInt(query[key], 10); + if (n === n) sum += n; + } + } + return sum; +} + +function startWorker() { + loadDataset(); + loadLargeDataset(); + loadStaticFiles(); + loadDatabase(); + + const express = require('express'); + const app = express(); + + // Raw body parsing + app.use(express.raw({ type: 'application/octet-stream', limit: '50mb' })); + app.use(express.text({ type: 'text/plain', limit: '50mb' })); + app.use(express.raw({ type: '*/*', limit: '50mb' })); + + // --- /pipeline --- + app.get('/pipeline', (req, res) => { + res.set('server', SERVER_NAME).type('text/plain').send('ok'); + }); + + // --- /baseline11 GET & POST --- + app.get('/baseline11', (req, res) => { + const s = sumQuery(req.query); + res.set('server', SERVER_NAME).type('text/plain').send(String(s)); + }); + + app.post('/baseline11', (req, res) => { + const querySum = sumQuery(req.query); + const body = typeof req.body === 'string' ? req.body : (req.body ? req.body.toString() : ''); + let total = querySum; + const n = parseInt(body.trim(), 10); + if (n === n) total += n; + res.set('server', SERVER_NAME).type('text/plain').send(String(total)); + }); + + // --- /baseline2 --- + app.get('/baseline2', (req, res) => { + const s = sumQuery(req.query); + res.set('server', SERVER_NAME).type('text/plain').send(String(s)); + }); + + // --- /json --- + app.get('/json', (req, res) => { + if (!datasetItems) { + return res.status(500).send('No dataset'); + } + const items = datasetItems.map(d => ({ + id: d.id, name: d.name, category: d.category, + price: d.price, quantity: d.quantity, active: d.active, + tags: d.tags, rating: d.rating, + total: Math.round(d.price * d.quantity * 100) / 100 + })); + const buf = Buffer.from(JSON.stringify({ items, count: items.length })); + res + .set('server', SERVER_NAME) + .writeHead(200, { 'content-type': 'application/json', 'content-length': buf.length }); + res.end(buf); + }); + + // --- /compression --- + app.get('/compression', (req, res) => { + if (!largeJsonBuf) { + return res.status(500).send('No dataset'); + } + const compressed = zlib.gzipSync(largeJsonBuf, { level: 1 }); + res + .set('server', SERVER_NAME) + .set('content-type', 'application/json') + .set('content-encoding', 'gzip') + .set('content-length', compressed.length) + .send(compressed); + }); + + // --- /db --- + app.get('/db', (req, res) => { + if (!dbStmt) { + return res.set('server', SERVER_NAME).type('application/json').send('{"items":[],"count":0}'); + } + let min = 10, max = 50; + if (req.query.min) min = parseFloat(req.query.min) || 10; + if (req.query.max) max = parseFloat(req.query.max) || 50; + const rows = dbStmt.all(min, max); + const items = rows.map(r => ({ + id: r.id, name: r.name, category: r.category, + price: r.price, quantity: r.quantity, active: r.active === 1, + tags: JSON.parse(r.tags), + rating: { score: r.rating_score, count: r.rating_count } + })); + const body = JSON.stringify({ items, count: items.length }); + res + .set('server', SERVER_NAME) + .set('content-type', 'application/json') + .set('content-length', Buffer.byteLength(body)) + .send(body); + }); + + // --- /upload --- + app.post('/upload', (req, res) => { + const body = Buffer.isBuffer(req.body) ? req.body : Buffer.from(req.body || ''); + res.set('server', SERVER_NAME).type('text/plain').send(String(body.length)); + }); + + // Start HTTP/1.1 server + const server = http.createServer(app); + server.listen(8080, '0.0.0.0', () => { + startH2(); + }); +} + +function startH2() { + const certFile = process.env.TLS_CERT || '/certs/server.crt'; + const keyFile = process.env.TLS_KEY || '/certs/server.key'; + try { + const opts = { + cert: fs.readFileSync(certFile), + key: fs.readFileSync(keyFile), + allowHTTP1: false, + }; + const h2server = http2.createSecureServer(opts, (req, res) => { + const url = req.url; + const q = url.indexOf('?'); + const p = q === -1 ? url : url.slice(0, q); + if (p.startsWith('/static/')) { + const name = p.slice(8); + const sf = staticFiles[name]; + if (sf) { + res.writeHead(200, { 'content-type': sf.ct, 'content-length': sf.buf.length, 'server': SERVER_NAME }); + res.end(sf.buf); + } else { + res.writeHead(404); + res.end(); + } + } else { + // baseline h2 + let sum = 0; + if (q !== -1) { + const qs = url.slice(q + 1); + let i = 0; + while (i < qs.length) { + const eq = qs.indexOf('=', i); + if (eq === -1) break; + let amp = qs.indexOf('&', eq); + if (amp === -1) amp = qs.length; + const n = parseInt(qs.slice(eq + 1, amp), 10); + if (n === n) sum += n; + i = amp + 1; + } + } + res.writeHead(200, { 'content-type': 'text/plain', 'server': SERVER_NAME }); + res.end(String(sum)); + } + }); + h2server.listen(8443); + } catch (e) { + // TLS certs not available, skip H2 + } +} + +if (cluster.isPrimary) { + const numCPUs = os.availableParallelism ? os.availableParallelism() : os.cpus().length; + for (let i = 0; i < numCPUs; i++) cluster.fork(); +} else { + startWorker(); +}