Skip to content

Commit 8596f8e

Browse files
committed
feat(api): initialize Fastify API with health check endpoints and testing setup
- Added Fastify server setup with CORS support. - Implemented health check endpoints (`GET /` and `GET /health`). - Created test suite using Vitest and Playwright for health check endpoints. - Included configuration files and README for development instructions. - Added .gitignore for node-related files.
1 parent 0916f3d commit 8596f8e

12 files changed

Lines changed: 1381 additions & 117 deletions

File tree

apps/api/.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules
2+
dist
3+
.env
4+
.env.local
5+
*.log

apps/api/README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# RefRef API
2+
3+
Fastify API server for RefRef platform.
4+
5+
## Development
6+
7+
```bash
8+
# Install dependencies
9+
pnpm install
10+
11+
# Run in development mode with hot reload
12+
pnpm dev
13+
14+
# Run tests
15+
pnpm test # Run tests in watch mode
16+
pnpm test:run # Run tests once
17+
pnpm test:ui # Run tests with UI
18+
19+
# Type checking
20+
pnpm typecheck
21+
22+
# Build for production
23+
pnpm build
24+
25+
# Start production server
26+
pnpm start
27+
```
28+
29+
## Environment Variables
30+
31+
```bash
32+
PORT=3001 # Server port (default: 3001)
33+
HOST=0.0.0.0 # Server host (default: 0.0.0.0)
34+
LOG_LEVEL=info # Logging level (default: info)
35+
NODE_ENV=development
36+
```
37+
38+
## Testing
39+
40+
The API uses Vitest and Playwright for integration testing. Tests start a real server instance and make HTTP requests to validate responses.
41+
42+
Test structure:
43+
- `test/utils/testServer.ts` - Test server utilities for starting/stopping the server
44+
- `test/health.test.ts` - Health endpoint tests
45+
46+
Each test suite:
47+
1. Starts a test server on a random port
48+
2. Creates a Playwright API request context
49+
3. Runs tests against real endpoints
50+
4. Cleans up resources after tests complete
51+
52+
## Endpoints
53+
54+
- `GET /` - Root health check endpoint
55+
- Returns: `{ok: true, service: "refref-api"}`
56+
- `GET /health` - Health check endpoint
57+
- Returns: `{ok: true, service: "refref-api"}`

apps/api/api.http

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
@host = http://localhost:3001
2+
3+
### Health Check - Root
4+
GET {{host}}/
5+
6+
### Health Check - Health Endpoint
7+
GET {{host}}/health

apps/api/package.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "@refref/api",
3+
"version": "0.1.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"dev": "tsx watch src/index.ts",
8+
"build": "tsc",
9+
"start": "node dist/index.js",
10+
"test": "vitest",
11+
"test:ui": "vitest --ui",
12+
"test:run": "vitest run",
13+
"typecheck": "tsc --noEmit",
14+
"lint": "eslint .",
15+
"format": "prettier --write ."
16+
},
17+
"dependencies": {
18+
"@fastify/cors": "^10.0.1",
19+
"fastify": "^5.2.0",
20+
"fastify-plugin": "^5.0.1"
21+
},
22+
"devDependencies": {
23+
"@types/node": "^22.10.2",
24+
"@vitest/ui": "^2.1.8",
25+
"pino-pretty": "^13.0.0",
26+
"playwright": "^1.49.1",
27+
"tsx": "^4.19.2",
28+
"typescript": "^5.7.2",
29+
"vitest": "^2.1.8"
30+
}
31+
}

apps/api/src/app.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import Fastify, { FastifyInstance } from "fastify";
2+
import cors from "@fastify/cors";
3+
import { healthHandler } from "./handlers/health.js";
4+
5+
export async function buildApp(): Promise<FastifyInstance> {
6+
const app = Fastify({
7+
logger: {
8+
level: process.env.LOG_LEVEL || "info",
9+
transport:
10+
process.env.NODE_ENV !== "production"
11+
? {
12+
target: "pino-pretty",
13+
options: {
14+
translateTime: "HH:MM:ss Z",
15+
ignore: "pid,hostname",
16+
colorize: true,
17+
},
18+
}
19+
: undefined,
20+
},
21+
});
22+
23+
// Register CORS plugin
24+
await app.register(cors, {
25+
origin: true,
26+
});
27+
28+
// Register health check routes
29+
app.get("/", healthHandler);
30+
app.get("/health", healthHandler);
31+
32+
return app;
33+
}

apps/api/src/handlers/health.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { FastifyReply, FastifyRequest } from "fastify";
2+
3+
export async function healthHandler(
4+
request: FastifyRequest,
5+
reply: FastifyReply
6+
) {
7+
return reply.send({
8+
ok: true,
9+
service: "refref-api",
10+
});
11+
}

apps/api/src/index.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { buildApp } from "./app.js";
2+
3+
const start = async () => {
4+
try {
5+
const app = await buildApp();
6+
7+
const port = Number(process.env.PORT) || 3001;
8+
const host = process.env.HOST || "0.0.0.0";
9+
10+
await app.listen({ port, host });
11+
12+
app.log.info(`Server listening on ${host}:${port}`);
13+
14+
// Graceful shutdown handlers
15+
const signals = ["SIGINT", "SIGTERM"] as const;
16+
for (const signal of signals) {
17+
process.on(signal, async () => {
18+
app.log.info(`Received ${signal}, closing server gracefully...`);
19+
await app.close();
20+
process.exit(0);
21+
});
22+
}
23+
} catch (err) {
24+
console.error(err);
25+
process.exit(1);
26+
}
27+
};
28+
29+
start();

apps/api/test/health.test.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
2+
import { request } from "playwright";
3+
import { startTestServer, stopTestServer } from "./utils/testServer.js";
4+
import type { APIRequestContext } from "playwright";
5+
6+
describe("Health Endpoints", () => {
7+
let apiContext: APIRequestContext;
8+
let baseURL: string;
9+
10+
beforeAll(async () => {
11+
// Start test server
12+
const { url } = await startTestServer();
13+
baseURL = url;
14+
15+
// Create Playwright API request context
16+
apiContext = await request.newContext({
17+
baseURL,
18+
});
19+
});
20+
21+
afterAll(async () => {
22+
// Clean up
23+
await apiContext.dispose();
24+
await stopTestServer();
25+
});
26+
27+
describe("GET /health", () => {
28+
it("should return ok: true and service name", async () => {
29+
const response = await apiContext.get("/health");
30+
31+
expect(response.ok()).toBe(true);
32+
expect(response.status()).toBe(200);
33+
34+
const body = await response.json();
35+
36+
expect(body).toEqual({
37+
ok: true,
38+
service: "refref-api",
39+
});
40+
});
41+
42+
it("should have correct content-type header", async () => {
43+
const response = await apiContext.get("/health");
44+
45+
const contentType = response.headers()["content-type"];
46+
expect(contentType).toContain("application/json");
47+
});
48+
49+
it("should respond quickly", async () => {
50+
const startTime = Date.now();
51+
const response = await apiContext.get("/health");
52+
const endTime = Date.now();
53+
54+
expect(response.ok()).toBe(true);
55+
expect(endTime - startTime).toBeLessThan(1000); // Should respond within 1 second
56+
});
57+
});
58+
59+
describe("GET /", () => {
60+
it("should return ok: true and service name", async () => {
61+
const response = await apiContext.get("/");
62+
63+
expect(response.ok()).toBe(true);
64+
expect(response.status()).toBe(200);
65+
66+
const body = await response.json();
67+
68+
expect(body).toEqual({
69+
ok: true,
70+
service: "refref-api",
71+
});
72+
});
73+
74+
it("should have correct content-type header", async () => {
75+
const response = await apiContext.get("/");
76+
77+
const contentType = response.headers()["content-type"];
78+
expect(contentType).toContain("application/json");
79+
});
80+
81+
it("should respond quickly", async () => {
82+
const startTime = Date.now();
83+
const response = await apiContext.get("/");
84+
const endTime = Date.now();
85+
86+
expect(response.ok()).toBe(true);
87+
expect(endTime - startTime).toBeLessThan(1000); // Should respond within 1 second
88+
});
89+
});
90+
});

apps/api/test/utils/testServer.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { FastifyInstance } from "fastify";
2+
import { buildApp } from "../../src/app.js";
3+
4+
let testServer: FastifyInstance | null = null;
5+
6+
export async function startTestServer(): Promise<{
7+
server: FastifyInstance;
8+
url: string;
9+
}> {
10+
if (testServer) {
11+
throw new Error("Test server is already running");
12+
}
13+
14+
testServer = await buildApp();
15+
16+
const port = Math.floor(Math.random() * 10000) + 30000; // Random port between 30000-40000
17+
await testServer.listen({ port, host: "127.0.0.1" });
18+
19+
const url = `http://127.0.0.1:${port}`;
20+
21+
return { server: testServer, url };
22+
}
23+
24+
export async function stopTestServer(): Promise<void> {
25+
if (testServer) {
26+
await testServer.close();
27+
testServer = null;
28+
}
29+
}

apps/api/tsconfig.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2022",
4+
"module": "ESNext",
5+
"lib": ["ES2022"],
6+
"moduleResolution": "bundler",
7+
"outDir": "./dist",
8+
"rootDir": "./src",
9+
"strict": true,
10+
"esModuleInterop": true,
11+
"skipLibCheck": true,
12+
"forceConsistentCasingInFileNames": true,
13+
"resolveJsonModule": true,
14+
"isolatedModules": true,
15+
"noUnusedLocals": true,
16+
"noUnusedParameters": true,
17+
"noImplicitReturns": true,
18+
"noFallthroughCasesInSwitch": true
19+
},
20+
"include": ["src/**/*"],
21+
"exclude": ["node_modules", "dist"]
22+
}

0 commit comments

Comments
 (0)