Skip to content
Open
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
33 changes: 33 additions & 0 deletions .github/workflows/api-benchmark-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: API Benchmark Smoke

on:
pull_request:
paths:
- "apps/api/**"
- "benchmarks/**"
- "package.json"
- "package-lock.json"
- ".github/workflows/api-benchmark-smoke.yml"

jobs:
benchmark-smoke:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- name: Install dependencies
run: npm ci

- name: Verify benchmark coverage
run: npm run benchmark:coverage

- name: Run API benchmark smoke gate
run: npm run benchmark:smoke
2 changes: 1 addition & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"scripts": {
"dev": "node src/server.js",
"start": "node src/server.js",
"test": "node --test src/tests"
"test": "node --test src/tests/*.test.js"
},
"dependencies": {
"cors": "^2.8.5",
Expand Down
10 changes: 10 additions & 0 deletions benchmarks/.env.benchmark.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Optional. When omitted, the benchmark runner starts the local Express app on a random port.
BENCHMARK_TARGET_URL=http://127.0.0.1:4000

# Optional override for protected benchmark routes. The local runner generates an admin token automatically.
BENCHMARK_AUTH_TOKEN=

# Defaults: smoke uses 1 concurrency and 2 requests per route; full uses 2 concurrency and 6 requests per route.
BENCHMARK_CONCURRENCY=2
BENCHMARK_REQUESTS_PER_ROUTE=6
BENCHMARK_REQUEST_TIMEOUT_MS=5000
28 changes: 28 additions & 0 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# API Benchmarks

This directory contains a reproducible benchmark suite for the Express API.

## Commands

- `npm run benchmark:coverage` verifies that every mounted `/api/*` route, plus `/health`, is represented in the benchmark manifest.
- `npm run benchmark:smoke` runs a low-concurrency gate suitable for CI.
- `npm run benchmark` runs the fuller local suite and writes JSON plus Markdown results to `benchmarks/results/`.

By default the runner starts the local Express app on a random loopback port and benchmarks that process. To benchmark another target, copy `.env.benchmark.example` to `.env.benchmark` and set `BENCHMARK_TARGET_URL`.

Protected routes use a dedicated benchmark admin token when the local app is started by the runner. For external targets, set `BENCHMARK_AUTH_TOKEN` to a token created for benchmark-only use.

## Metrics

Each endpoint records:

- p50, p95, and p99 total latency
- p50, p95, and p99 time to first byte
- sustained and peak requests per second
- status-code distribution
- error rate
- bytes received

Thresholds for the CI smoke gate live in `benchmarks/thresholds.json`.

The PR demo artifact is `demos/api-benchmark-demo.mp4`.
240 changes: 240 additions & 0 deletions benchmarks/api-route-manifest.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
export const API_ROUTES = [
{
id: "health",
method: "GET",
path: "/health",
pathTemplate: "/health",
description: "Service liveness check",
expectedStatus: 200
},
{
id: "auth-register",
method: "POST",
path: "/api/auth/register",
pathTemplate: "/api/auth/register",
description: "Register a benchmark client user",
expectedStatus: 201,
json: (index) => ({
email: `bench-client-${Date.now()}-${index}@example.com`,
password: "benchmark-password",
role: "client"
})
},
{
id: "auth-login",
method: "POST",
path: "/api/auth/login",
pathTemplate: "/api/auth/login",
description: "Login with representative credentials",
expectedStatus: 200,
json: () => ({
email: "benchmark@example.com",
password: "benchmark-password"
})
},
{
id: "auth-refresh",
method: "POST",
path: "/api/auth/refresh",
pathTemplate: "/api/auth/refresh",
description: "Refresh an access token",
expectedStatus: 200
},
{
id: "auth-oauth-callback",
method: "GET",
path: "/api/auth/oauth/github/callback",
pathTemplate: "/api/auth/oauth/:provider/callback",
description: "OAuth callback acknowledgement",
expectedStatus: 200
},
{
id: "users-list",
method: "GET",
path: "/api/users",
pathTemplate: "/api/users/",
description: "List users",
expectedStatus: 200
},
{
id: "users-create",
method: "POST",
path: "/api/users",
pathTemplate: "/api/users/",
description: "Create a representative user profile",
expectedStatus: 201,
json: (index) => ({
name: `Benchmark User ${index}`,
email: `bench-user-${Date.now()}-${index}@example.com`,
role: index % 2 === 0 ? "client" : "freelancer",
status: "active",
bio: "Benchmark profile seeded with realistic text length for API regression tracking."
})
},
{
id: "jobs-list",
method: "GET",
path: "/api/jobs",
pathTemplate: "/api/jobs/",
description: "List jobs",
expectedStatus: 200
},
{
id: "jobs-create",
method: "POST",
path: "/api/jobs",
pathTemplate: "/api/jobs/",
description: "Create a representative marketplace job",
expectedStatus: 201,
json: (index) => ({
title: `Benchmark API integration build ${index}`,
description:
"Build a production-ready integration with validation, webhook handling, retries, and clear operational docs.",
budgetMin: 750,
budgetMax: 2500,
categoryId: "cat_web_development",
skills: ["node", "express", "api", "testing"]
})
},
{
id: "proposals-list",
method: "GET",
path: "/api/proposals",
pathTemplate: "/api/proposals/",
description: "List proposals",
expectedStatus: 200
},
{
id: "proposals-create",
method: "POST",
path: "/api/proposals",
pathTemplate: "/api/proposals/",
description: "Submit a representative proposal",
expectedStatus: 201,
json: (index) => ({
jobId: `job_benchmark_${index}`,
freelancerId: `usr_freelancer_${index}`,
amount: 1200,
timelineDays: 5,
coverLetter:
"I can deliver the integration with endpoint tests, retry handling, and a concise deployment checklist."
})
},
{
id: "payments-create",
method: "POST",
path: "/api/payments",
pathTemplate: "/api/payments/",
description: "Create a payment intent",
expectedStatus: 201,
json: (index) => ({
jobId: `job_benchmark_${index}`,
amount: 1200,
currency: "usd",
payerId: `usr_client_${index}`
})
},
{
id: "reviews-list",
method: "GET",
path: "/api/reviews",
pathTemplate: "/api/reviews/",
description: "List reviews",
expectedStatus: 200
},
{
id: "reviews-create",
method: "POST",
path: "/api/reviews",
pathTemplate: "/api/reviews/",
description: "Create a representative review",
expectedStatus: 201,
json: (index) => ({
jobId: `job_benchmark_${index}`,
reviewerId: `usr_client_${index}`,
revieweeId: `usr_freelancer_${index}`,
rating: 5,
comment: "Fast, clear delivery with strong tests and useful handoff notes."
})
},
{
id: "messages-list",
method: "GET",
path: "/api/messages",
pathTemplate: "/api/messages/",
description: "List messages",
expectedStatus: 200
},
{
id: "messages-create",
method: "POST",
path: "/api/messages",
pathTemplate: "/api/messages/",
description: "Send a representative project message",
expectedStatus: 201,
json: (index) => ({
threadId: `thread_benchmark_${index}`,
senderId: `usr_client_${index}`,
recipientId: `usr_freelancer_${index}`,
body: "Can you confirm the API retry behavior and include the benchmark summary in the handoff?"
})
},
{
id: "notifications-list",
method: "GET",
path: "/api/notifications",
pathTemplate: "/api/notifications/",
description: "List notifications",
expectedStatus: 200
},
{
id: "notifications-create",
method: "POST",
path: "/api/notifications",
pathTemplate: "/api/notifications/",
description: "Create a representative notification",
expectedStatus: 201,
json: (index) => ({
userId: `usr_client_${index}`,
type: "proposal_received",
title: "New proposal received",
body: "A freelancer submitted a proposal for your benchmark API integration job."
})
},
{
id: "uploads-create",
method: "POST",
path: "/api/uploads",
pathTemplate: "/api/uploads/",
description: "Upload a small representative attachment",
expectedStatus: 201,
multipart: {
fieldName: "file",
filename: "benchmark-brief.txt",
contentType: "text/plain",
content:
"Benchmark attachment body that simulates a short project brief uploaded by a client."
}
},
{
id: "search-global",
method: "GET",
path: "/api/search?q=benchmark%20api%20developer",
pathTemplate: "/api/search/",
description: "Run global search with a realistic query",
expectedStatus: 200
},
{
id: "admin-metrics",
method: "GET",
path: "/api/admin/metrics",
pathTemplate: "/api/admin/metrics",
description: "Fetch protected admin metrics with benchmark token",
expectedStatus: 200,
auth: "admin"
}
];

export function routeLabels() {
return API_ROUTES.map((route) => `${route.method} ${route.pathTemplate}`);
}
61 changes: 61 additions & 0 deletions benchmarks/check-api-coverage.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { API_ROUTES, routeLabels } from "./api-route-manifest.mjs";

const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");

function read(filePath) {
return fs.readFileSync(path.join(rootDir, filePath), "utf8");
}

function normalizeMountedPath(basePath, routePath) {
const joined = `${basePath.replace(/\/$/, "")}/${routePath.replace(/^\//, "")}`;
return joined.replace(/\/$/, "");
}

function mountedRouters() {
const appSource = read("apps/api/src/app.js");
const mounts = [...appSource.matchAll(/app\.use\("([^"]+)",\s*([a-zA-Z]+)Routes\)/g)];
return mounts.map(([, basePath, routerName]) => ({
basePath,
routerFile: `apps/api/src/routes/${routerName}Routes.js`
}));
}

function routeDefinitions() {
const routes = new Set(["GET /health"]);

for (const mount of mountedRouters()) {
const source = read(mount.routerFile);
const matches = [...source.matchAll(/(?:\w+Routes|Router\(\)|adminRoutes)\.(get|post|put|patch|delete)\("([^"]+)"/gi)];
for (const [, method, routePath] of matches) {
routes.add(`${method.toUpperCase()} ${normalizeMountedPath(mount.basePath, routePath)}`);
}
}

return routes;
}

const implemented = new Set([...routeDefinitions()].map((route) => route.replace(/\/$/, "")));
const benchmarked = new Set(routeLabels().map((label) => label.replace(/\/$/, "")));
const missing = [...implemented].filter((route) => !benchmarked.has(route));
const stale = [...benchmarked].filter((route) => !implemented.has(route));

if (missing.length || stale.length) {
console.error("Benchmark manifest is out of sync with the Express route surface.");
if (missing.length) {
console.error("\nMissing from benchmarks:");
for (const route of missing) console.error(`- ${route}`);
}
if (stale.length) {
console.error("\nNo matching Express route:");
for (const route of stale) console.error(`- ${route}`);
}
process.exit(1);
}

console.log(`Benchmark manifest covers ${API_ROUTES.length} routes:`);
for (const route of routeLabels()) {
console.log(`- ${route}`);
}
1 change: 1 addition & 0 deletions benchmarks/results/.gitkeep
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Loading
Loading