Skip to content

Commit 11f5747

Browse files
authored
Merge pull request #30 from BennyFranciscus/add-fastify
Add Fastify: the performance-focused Node.js web framework (~33k⭐)
2 parents 953e503 + 4d438db commit 11f5747

5 files changed

Lines changed: 283 additions & 0 deletions

File tree

frameworks/fastify/Dockerfile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
FROM node:22-slim
2+
RUN apt-get update && apt-get install -y --no-install-recommends python3 make g++ && rm -rf /var/lib/apt/lists/*
3+
WORKDIR /app
4+
COPY package.json .
5+
RUN npm install --omit=dev
6+
COPY server.js .
7+
ENV NODE_ENV=production
8+
EXPOSE 8080
9+
CMD ["node", "server.js"]

frameworks/fastify/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Fastify
2+
3+
[Fastify](https://github.com/fastify/fastify) is a fast and low-overhead web framework for Node.js. It's designed to be highly performant with a powerful plugin architecture and built-in schema validation via JSON Schema.
4+
5+
- **Language:** JavaScript (Node.js)
6+
- **Version:** 5.x
7+
- **Concurrency:** Node.js cluster module (one worker per CPU)
8+
- **Notable:** Built-in JSON serialization optimization, schema-based validation, encapsulation via plugins

frameworks/fastify/meta.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"display_name": "Fastify",
3+
"language": "JS",
4+
"type": "framework",
5+
"engine": "V8",
6+
"description": "Fast and low-overhead Node.js web framework built for developer experience and performance.",
7+
"repo": "https://github.com/fastify/fastify",
8+
"enabled": true,
9+
"tests": [
10+
"baseline",
11+
"pipelined",
12+
"limited-conn",
13+
"json",
14+
"upload",
15+
"compression",
16+
"mixed",
17+
"noisy",
18+
"baseline-h2",
19+
"static-h2"
20+
]
21+
}

frameworks/fastify/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "httparena-fastify",
3+
"version": "1.0.0",
4+
"private": true,
5+
"dependencies": {
6+
"fastify": "^5.2.0",
7+
"better-sqlite3": "^11.0.0"
8+
}
9+
}

frameworks/fastify/server.js

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
const cluster = require('cluster');
2+
const os = require('os');
3+
const fs = require('fs');
4+
const http2 = require('http2');
5+
const zlib = require('zlib');
6+
7+
const SERVER_NAME = 'fastify';
8+
9+
// --- Shared data (loaded per-worker) ---
10+
let datasetItems;
11+
let largeJsonBuf;
12+
let dbStmt;
13+
const staticFiles = {};
14+
const MIME_TYPES = {
15+
'.css': 'text/css', '.js': 'application/javascript', '.html': 'text/html',
16+
'.woff2': 'font/woff2', '.svg': 'image/svg+xml', '.webp': 'image/webp', '.json': 'application/json'
17+
};
18+
19+
function loadStaticFiles() {
20+
const dir = '/data/static';
21+
try {
22+
for (const name of fs.readdirSync(dir)) {
23+
const buf = fs.readFileSync(dir + '/' + name);
24+
const ext = name.slice(name.lastIndexOf('.'));
25+
staticFiles[name] = { buf, ct: MIME_TYPES[ext] || 'application/octet-stream' };
26+
}
27+
} catch (e) {}
28+
}
29+
30+
function loadDataset() {
31+
const path = process.env.DATASET_PATH || '/data/dataset.json';
32+
try {
33+
datasetItems = JSON.parse(fs.readFileSync(path, 'utf8'));
34+
} catch (e) {}
35+
}
36+
37+
function loadLargeDataset() {
38+
try {
39+
const raw = JSON.parse(fs.readFileSync('/data/dataset-large.json', 'utf8'));
40+
const items = raw.map(d => ({
41+
id: d.id, name: d.name, category: d.category,
42+
price: d.price, quantity: d.quantity, active: d.active,
43+
tags: d.tags, rating: d.rating,
44+
total: Math.round(d.price * d.quantity * 100) / 100
45+
}));
46+
largeJsonBuf = Buffer.from(JSON.stringify({ items, count: items.length }));
47+
} catch (e) {}
48+
}
49+
50+
function loadDatabase() {
51+
try {
52+
const Database = require('better-sqlite3');
53+
const db = new Database('/data/benchmark.db', { readonly: true });
54+
db.pragma('mmap_size=268435456');
55+
dbStmt = db.prepare('SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count FROM items WHERE price BETWEEN ? AND ? LIMIT 50');
56+
} catch (e) {}
57+
}
58+
59+
function sumQuery(query) {
60+
let sum = 0;
61+
if (query) {
62+
for (const key of Object.keys(query)) {
63+
const n = parseInt(query[key], 10);
64+
if (n === n) sum += n;
65+
}
66+
}
67+
return sum;
68+
}
69+
70+
function startWorker() {
71+
loadDataset();
72+
loadLargeDataset();
73+
loadStaticFiles();
74+
loadDatabase();
75+
76+
const Fastify = require('fastify');
77+
const app = Fastify({ logger: false, bodyLimit: 50 * 1024 * 1024 });
78+
79+
// Register raw body parsers so req.body is available without manual stream reading
80+
app.addContentTypeParser('text/plain', { parseAs: 'string' }, (req, body, done) => done(null, body));
81+
app.addContentTypeParser('application/octet-stream', { parseAs: 'buffer' }, (req, body, done) => done(null, body));
82+
app.addContentTypeParser('*', { parseAs: 'buffer' }, (req, body, done) => done(null, body));
83+
84+
// --- /pipeline ---
85+
app.get('/pipeline', (req, reply) => {
86+
reply.header('server', SERVER_NAME).type('text/plain').send('ok');
87+
});
88+
89+
// --- /baseline11 GET & POST ---
90+
app.get('/baseline11', (req, reply) => {
91+
const s = sumQuery(req.query);
92+
reply.header('server', SERVER_NAME).type('text/plain').send(String(s));
93+
});
94+
95+
app.post('/baseline11', (req, reply) => {
96+
const querySum = sumQuery(req.query);
97+
const body = typeof req.body === 'string' ? req.body : (req.body ? req.body.toString() : '');
98+
let total = querySum;
99+
const n = parseInt(body.trim(), 10);
100+
if (n === n) total += n;
101+
reply.header('server', SERVER_NAME).type('text/plain').send(String(total));
102+
});
103+
104+
// --- /baseline2 ---
105+
app.get('/baseline2', (req, reply) => {
106+
const s = sumQuery(req.query);
107+
reply.header('server', SERVER_NAME).type('text/plain').send(String(s));
108+
});
109+
110+
// --- /json ---
111+
app.get('/json', (req, reply) => {
112+
if (!datasetItems) {
113+
reply.code(500).send('No dataset');
114+
return;
115+
}
116+
const items = datasetItems.map(d => ({
117+
id: d.id, name: d.name, category: d.category,
118+
price: d.price, quantity: d.quantity, active: d.active,
119+
tags: d.tags, rating: d.rating,
120+
total: Math.round(d.price * d.quantity * 100) / 100
121+
}));
122+
const buf = Buffer.from(JSON.stringify({ items, count: items.length }));
123+
reply
124+
.header('server', SERVER_NAME)
125+
.header('content-type', 'application/json')
126+
.header('content-length', buf.length)
127+
.send(buf);
128+
});
129+
130+
// --- /compression ---
131+
app.get('/compression', (req, reply) => {
132+
if (!largeJsonBuf) {
133+
reply.code(500).send('No dataset');
134+
return;
135+
}
136+
const compressed = zlib.gzipSync(largeJsonBuf, { level: 1 });
137+
reply
138+
.header('server', SERVER_NAME)
139+
.header('content-type', 'application/json')
140+
.header('content-encoding', 'gzip')
141+
.header('content-length', compressed.length)
142+
.send(compressed);
143+
});
144+
145+
// --- /db ---
146+
app.get('/db', (req, reply) => {
147+
if (!dbStmt) {
148+
reply.header('server', SERVER_NAME).type('application/json').send('{"items":[],"count":0}');
149+
return;
150+
}
151+
let min = 10, max = 50;
152+
if (req.query.min) min = parseFloat(req.query.min) || 10;
153+
if (req.query.max) max = parseFloat(req.query.max) || 50;
154+
const rows = dbStmt.all(min, max);
155+
const items = rows.map(r => ({
156+
id: r.id, name: r.name, category: r.category,
157+
price: r.price, quantity: r.quantity, active: r.active === 1,
158+
tags: JSON.parse(r.tags),
159+
rating: { score: r.rating_score, count: r.rating_count }
160+
}));
161+
const body = JSON.stringify({ items, count: items.length });
162+
reply
163+
.header('server', SERVER_NAME)
164+
.header('content-type', 'application/json')
165+
.header('content-length', Buffer.byteLength(body))
166+
.send(body);
167+
});
168+
169+
// --- /upload ---
170+
app.post('/upload', (req, reply) => {
171+
const body = Buffer.isBuffer(req.body) ? req.body : Buffer.from(req.body || '');
172+
reply.header('server', SERVER_NAME).type('text/plain').send(String(body.length));
173+
});
174+
175+
// Start HTTP/1.1 server
176+
app.listen({ port: 8080, host: '0.0.0.0' }).then(() => {
177+
// Also start HTTP/2 server on 8443
178+
startH2();
179+
});
180+
}
181+
182+
function startH2() {
183+
const certFile = process.env.TLS_CERT || '/certs/server.crt';
184+
const keyFile = process.env.TLS_KEY || '/certs/server.key';
185+
try {
186+
const opts = {
187+
cert: fs.readFileSync(certFile),
188+
key: fs.readFileSync(keyFile),
189+
allowHTTP1: false,
190+
};
191+
const h2server = http2.createSecureServer(opts, (req, res) => {
192+
const url = req.url;
193+
const q = url.indexOf('?');
194+
const p = q === -1 ? url : url.slice(0, q);
195+
if (p.startsWith('/static/')) {
196+
const name = p.slice(8);
197+
const sf = staticFiles[name];
198+
if (sf) {
199+
res.writeHead(200, { 'content-type': sf.ct, 'content-length': sf.buf.length, 'server': SERVER_NAME });
200+
res.end(sf.buf);
201+
} else {
202+
res.writeHead(404);
203+
res.end();
204+
}
205+
} else {
206+
// baseline h2
207+
let sum = 0;
208+
if (q !== -1) {
209+
const qs = url.slice(q + 1);
210+
let i = 0;
211+
while (i < qs.length) {
212+
const eq = qs.indexOf('=', i);
213+
if (eq === -1) break;
214+
let amp = qs.indexOf('&', eq);
215+
if (amp === -1) amp = qs.length;
216+
const n = parseInt(qs.slice(eq + 1, amp), 10);
217+
if (n === n) sum += n;
218+
i = amp + 1;
219+
}
220+
}
221+
res.writeHead(200, { 'content-type': 'text/plain', 'server': SERVER_NAME });
222+
res.end(String(sum));
223+
}
224+
});
225+
h2server.listen(8443);
226+
} catch (e) {
227+
// TLS certs not available, skip H2
228+
}
229+
}
230+
231+
if (cluster.isPrimary) {
232+
const numCPUs = os.availableParallelism ? os.availableParallelism() : os.cpus().length;
233+
for (let i = 0; i < numCPUs; i++) cluster.fork();
234+
} else {
235+
startWorker();
236+
}

0 commit comments

Comments
 (0)