Skip to content

Commit ee64ac2

Browse files
RoyRoy
authored andcommitted
Add API benchmark suite
1 parent 7536da5 commit ee64ac2

16 files changed

Lines changed: 1619 additions & 1 deletion

.env.benchmark.example

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Target an already-running API. Leave empty to start the local Express app in-process.
2+
BENCHMARK_TARGET_URL=
3+
4+
# Optional auth token override for protected routes. Leave empty to generate a local admin JWT.
5+
BENCHMARK_AUTH_TOKEN=
6+
7+
# Request load settings. Defaults are intentionally under the app rate-limit ceiling.
8+
BENCHMARK_REQUESTS_PER_ENDPOINT=8
9+
BENCHMARK_CONCURRENCY=4
10+
11+
# Smoke mode uses lower defaults unless these are set.
12+
BENCHMARK_SMOKE_REQUESTS_PER_ENDPOINT=2
13+
BENCHMARK_SMOKE_CONCURRENCY=1
14+
15+
# Output location for JSON and Markdown reports.
16+
BENCHMARK_RESULTS_DIR=benchmarks/results
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: API benchmark smoke
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- main
8+
9+
jobs:
10+
api-benchmark-smoke:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- uses: actions/setup-node@v4
17+
with:
18+
node-version: 20
19+
cache: npm
20+
21+
- run: npm ci
22+
23+
- run: npm run benchmark:smoke

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ node_modules
33
dist
44
.env
55
.env.*
6+
!.env.benchmark.example
67
coverage
78
*.log

apps/api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"scripts": {
66
"dev": "node src/server.js",
77
"start": "node src/server.js",
8-
"test": "node --test src/tests"
8+
"test": "node --test src/tests/*.js"
99
},
1010
"dependencies": {
1111
"cors": "^2.8.5",
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import test from "node:test";
2+
import assert from "node:assert/strict";
3+
import { benchmarkEndpoints } from "../../../../benchmarks/endpoints.mjs";
4+
5+
test("benchmark suite covers every mounted API route", () => {
6+
const expectedRoutes = new Set([
7+
"POST /api/auth/register",
8+
"POST /api/auth/login",
9+
"GET /api/auth/oauth/github/callback",
10+
"POST /api/auth/refresh",
11+
"GET /api/users",
12+
"POST /api/users",
13+
"GET /api/jobs",
14+
"POST /api/jobs",
15+
"GET /api/proposals",
16+
"POST /api/proposals",
17+
"POST /api/payments",
18+
"GET /api/reviews",
19+
"POST /api/reviews",
20+
"GET /api/messages",
21+
"POST /api/messages",
22+
"GET /api/notifications",
23+
"POST /api/notifications",
24+
"POST /api/uploads",
25+
"GET /api/search?q=react%20payments%20benchmark",
26+
"GET /api/admin/metrics"
27+
]);
28+
29+
const benchmarkRoutes = new Set(
30+
benchmarkEndpoints
31+
.filter((endpoint) => endpoint.path.startsWith("/api/"))
32+
.map((endpoint) => `${endpoint.method} ${endpoint.path}`)
33+
);
34+
35+
assert.deepEqual(benchmarkRoutes, expectedRoutes);
36+
});

benchmarks/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# API Benchmark Suite
2+
3+
This suite benchmarks the Express API routes mounted by `apps/api/src/app.js`.
4+
It can start the local app in-process or target a running API with
5+
`BENCHMARK_TARGET_URL`.
6+
7+
## Commands
8+
9+
```bash
10+
npm run benchmark
11+
npm run benchmark:smoke
12+
```
13+
14+
`benchmark:smoke` is meant for CI. It uses low concurrency and the same
15+
threshold checks as the full benchmark.
16+
17+
## Configuration
18+
19+
Copy `.env.benchmark.example` to `.env.benchmark` and adjust values as needed.
20+
Environment variables already exported in the shell take precedence.
21+
22+
Protected routes use `BENCHMARK_AUTH_TOKEN` when provided. If the benchmark
23+
starts the local app, it generates a dedicated admin-scoped benchmark JWT.
24+
25+
## Outputs
26+
27+
Every run writes timestamped JSON and Markdown reports under
28+
`benchmarks/results/`, plus `latest.json` and `latest.md` convenience copies.
29+
Each report includes p50, p95, p99 latency, p95 TTFB, sustained and peak RPS,
30+
error rate, and non-2xx rate per endpoint.
31+
32+
Thresholds live in `benchmarks/thresholds.json` so reviewers can tune the smoke
33+
gate without editing benchmark code.

benchmarks/endpoints.mjs

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
const longJobDescription =
2+
"Build a responsive marketplace dashboard with saved filters, proposal review, milestone tracking, and client messaging. Include accessibility checks and realistic empty states.";
3+
4+
function jsonBody(payload) {
5+
return {
6+
headers: { "content-type": "application/json" },
7+
body: JSON.stringify(payload)
8+
};
9+
}
10+
11+
function uploadBody() {
12+
const form = new FormData();
13+
const content = "benchmark upload payload\n".repeat(64);
14+
form.append("file", new Blob([content], { type: "text/plain" }), "benchmark-notes.txt");
15+
return { body: form };
16+
}
17+
18+
export const benchmarkEndpoints = [
19+
{
20+
name: "health",
21+
method: "GET",
22+
path: "/health",
23+
description: "Service health probe"
24+
},
25+
{
26+
name: "auth_register",
27+
method: "POST",
28+
path: "/api/auth/register",
29+
description: "Register a benchmark client user",
30+
request: ({ iteration }) =>
31+
jsonBody({
32+
email: `benchmark.client.${iteration}@example.com`,
33+
password: "benchmark-password",
34+
role: "client"
35+
})
36+
},
37+
{
38+
name: "auth_login",
39+
method: "POST",
40+
path: "/api/auth/login",
41+
description: "Login with realistic credentials",
42+
request: () =>
43+
jsonBody({
44+
email: "existing.client@example.com",
45+
password: "benchmark-password"
46+
})
47+
},
48+
{
49+
name: "auth_oauth_callback",
50+
method: "GET",
51+
path: "/api/auth/oauth/github/callback",
52+
description: "OAuth provider callback stub"
53+
},
54+
{
55+
name: "auth_refresh",
56+
method: "POST",
57+
path: "/api/auth/refresh",
58+
description: "Refresh an access token"
59+
},
60+
{
61+
name: "users_list",
62+
method: "GET",
63+
path: "/api/users",
64+
description: "List users"
65+
},
66+
{
67+
name: "users_create",
68+
method: "POST",
69+
path: "/api/users",
70+
description: "Create a freelancer profile-sized user record",
71+
request: ({ iteration }) =>
72+
jsonBody({
73+
email: `benchmark.freelancer.${iteration}@example.com`,
74+
name: "Benchmark Freelancer",
75+
role: "freelancer",
76+
hourlyRate: 85,
77+
skills: ["node", "react", "api-performance", "payments"],
78+
bio: "Senior full-stack freelancer with marketplace and payments experience."
79+
})
80+
},
81+
{
82+
name: "jobs_list",
83+
method: "GET",
84+
path: "/api/jobs",
85+
description: "List jobs"
86+
},
87+
{
88+
name: "jobs_create",
89+
method: "POST",
90+
path: "/api/jobs",
91+
description: "Create a realistic marketplace job",
92+
request: ({ iteration }) =>
93+
jsonBody({
94+
title: `Benchmark dashboard build ${iteration}`,
95+
description: longJobDescription,
96+
budgetMin: 1500,
97+
budgetMax: 4500,
98+
categoryId: "web-apps",
99+
skills: ["node", "react", "postgres", "payments"]
100+
})
101+
},
102+
{
103+
name: "proposals_list",
104+
method: "GET",
105+
path: "/api/proposals",
106+
description: "List proposals"
107+
},
108+
{
109+
name: "proposals_create",
110+
method: "POST",
111+
path: "/api/proposals",
112+
description: "Create a proposal payload",
113+
request: ({ iteration }) =>
114+
jsonBody({
115+
jobId: `job_benchmark_${iteration}`,
116+
freelancerId: "usr_benchmark_freelancer",
117+
coverLetter:
118+
"I can deliver the API benchmark dashboard in two milestones with automated validation and weekly demos.",
119+
proposedBudget: 3200,
120+
estimatedDays: 14
121+
})
122+
},
123+
{
124+
name: "payments_create",
125+
method: "POST",
126+
path: "/api/payments",
127+
description: "Create a payment intent-sized payload",
128+
request: ({ iteration }) =>
129+
jsonBody({
130+
jobId: `job_benchmark_${iteration}`,
131+
clientId: "usr_benchmark_client",
132+
freelancerId: "usr_benchmark_freelancer",
133+
amount: 3200,
134+
currency: "usd"
135+
})
136+
},
137+
{
138+
name: "reviews_list",
139+
method: "GET",
140+
path: "/api/reviews",
141+
description: "List reviews"
142+
},
143+
{
144+
name: "reviews_create",
145+
method: "POST",
146+
path: "/api/reviews",
147+
description: "Create a post-project review",
148+
request: ({ iteration }) =>
149+
jsonBody({
150+
jobId: `job_benchmark_${iteration}`,
151+
reviewerId: "usr_benchmark_client",
152+
revieweeId: "usr_benchmark_freelancer",
153+
rating: 5,
154+
comment: "Delivered quickly, communicated clearly, and kept the API stable under load."
155+
})
156+
},
157+
{
158+
name: "messages_list",
159+
method: "GET",
160+
path: "/api/messages",
161+
description: "List messages"
162+
},
163+
{
164+
name: "messages_create",
165+
method: "POST",
166+
path: "/api/messages",
167+
description: "Send a project message",
168+
request: ({ iteration }) =>
169+
jsonBody({
170+
threadId: `thread_benchmark_${iteration % 3}`,
171+
senderId: "usr_benchmark_client",
172+
recipientId: "usr_benchmark_freelancer",
173+
body: "Can you share the latest milestone status and benchmark report before the review call?"
174+
})
175+
},
176+
{
177+
name: "notifications_list",
178+
method: "GET",
179+
path: "/api/notifications",
180+
description: "List notifications"
181+
},
182+
{
183+
name: "notifications_create",
184+
method: "POST",
185+
path: "/api/notifications",
186+
description: "Create a notification",
187+
request: ({ iteration }) =>
188+
jsonBody({
189+
userId: "usr_benchmark_freelancer",
190+
type: "proposal_update",
191+
title: "Proposal shortlisted",
192+
message: `Benchmark notification ${iteration}: your proposal moved to review.`
193+
})
194+
},
195+
{
196+
name: "uploads_create",
197+
method: "POST",
198+
path: "/api/uploads",
199+
description: "Upload a representative text attachment",
200+
request: uploadBody
201+
},
202+
{
203+
name: "search",
204+
method: "GET",
205+
path: "/api/search?q=react%20payments%20benchmark",
206+
description: "Search with realistic keywords"
207+
},
208+
{
209+
name: "admin_metrics",
210+
method: "GET",
211+
path: "/api/admin/metrics",
212+
description: "Protected admin metrics route",
213+
auth: true
214+
}
215+
];

0 commit comments

Comments
 (0)