Skip to content

Commit d5b055d

Browse files
committed
Add Swagger and OpenAPI documentation
1 parent bdd6398 commit d5b055d

23 files changed

Lines changed: 617 additions & 274 deletions

File tree

dist/templates/api/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,11 @@ Build:
106106
LaunchStack CLI
107107

108108
https://www.npmjs.com/package/launchstack-cli
109+
110+
## API Documentation
111+
112+
Swagger UI:
113+
114+
http://localhost:3000/docs
115+
116+
The generated OpenAPI document includes health endpoints, authentication endpoints, request schemas, response schemas, JWT bearer authentication, and documented error responses.

dist/templates/api/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
"prisma:studio": "prisma studio",
1818
"db:up": "docker compose up -d postgres",
1919
"db:down": "docker compose down",
20-
"db:logs": "docker compose logs -f postgres"
20+
"db:logs": "docker compose logs -f postgres",
21+
"openapi:check": "npm run typecheck && npm test && npm run build"
2122
},
2223
"dependencies": {
2324
"@fastify/cors": "^11.1.0",
@@ -27,7 +28,9 @@
2728
"bcryptjs": "^3.0.2",
2829
"dotenv": "^16.4.0",
2930
"fastify": "^5.6.1",
30-
"fastify-plugin": "^5.0.1"
31+
"fastify-plugin": "^5.0.1",
32+
"@fastify/swagger": "^9.5.1",
33+
"@fastify/swagger-ui": "^5.2.3"
3134
},
3235
"devDependencies": {
3336
"@types/node": "^25.0.0",
Lines changed: 23 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,52 @@
11
import type { FastifyPluginAsync } from "fastify";
2-
import {
3-
loginSchema,
4-
refreshSchema,
5-
registerSchema
6-
} from "./auth.schemas";
7-
import {
8-
loginUser,
9-
refreshUserTokens,
10-
registerUser,
11-
revokeRefreshToken
12-
} from "./auth.service";
13-
import type {
14-
LoginInput,
15-
RefreshInput,
16-
RegisterInput
17-
} from "./auth.types";
2+
import { loginSchema, logoutSchema, meSchema, refreshSchema, registerSchema } from "./auth.schemas";
3+
import { loginUser, refreshUserTokens, registerUser, revokeRefreshToken } from "./auth.service";
4+
import type { LoginInput, RefreshInput, RegisterInput } from "./auth.types";
185

196
export const authRoutes: FastifyPluginAsync = async (app) => {
207
app.post<{ Body: RegisterInput }>(
218
"/register",
22-
{
23-
schema: registerSchema
24-
},
9+
{ schema: registerSchema },
2510
async (request, reply) => {
26-
const result = await registerUser(
27-
app,
28-
request.body
29-
);
30-
11+
const result = await registerUser(app, request.body);
3112
return reply.status(201).send(result);
3213
}
3314
);
3415

3516
app.post<{ Body: LoginInput }>(
3617
"/login",
37-
{
38-
schema: loginSchema
39-
},
40-
async (request) => {
41-
return loginUser(app, request.body);
42-
}
18+
{ schema: loginSchema },
19+
async (request) => loginUser(app, request.body)
4320
);
4421

4522
app.post<{ Body: RefreshInput }>(
4623
"/refresh",
47-
{
48-
schema: refreshSchema
49-
},
50-
async (request) => {
51-
return refreshUserTokens(
52-
app,
53-
request.body.refreshToken
54-
);
55-
}
24+
{ schema: refreshSchema },
25+
async (request) => refreshUserTokens(app, request.body.refreshToken)
5626
);
5727

5828
app.post<{ Body: RefreshInput }>(
5929
"/logout",
60-
{
61-
schema: refreshSchema
62-
},
30+
{ schema: logoutSchema },
6331
async (request, reply) => {
64-
await revokeRefreshToken(
65-
app,
66-
request.body.refreshToken
67-
);
68-
32+
await revokeRefreshToken(app, request.body.refreshToken);
6933
return reply.status(204).send();
7034
}
7135
);
7236

7337
app.get(
7438
"/me",
75-
{
76-
preHandler: [app.authenticate]
77-
},
78-
async (request) => {
79-
return app.prisma.user.findUniqueOrThrow({
80-
where: {
81-
id: request.user.sub
82-
},
83-
select: {
84-
id: true,
85-
email: true,
86-
name: true,
87-
role: true,
88-
createdAt: true,
89-
updatedAt: true
90-
}
91-
});
92-
}
39+
{ schema: meSchema, preHandler: [app.authenticate] },
40+
async (request) => app.prisma.user.findUniqueOrThrow({
41+
where: { id: request.user.sub },
42+
select: {
43+
id: true,
44+
email: true,
45+
name: true,
46+
role: true,
47+
createdAt: true,
48+
updatedAt: true
49+
}
50+
})
9351
);
9452
};
Lines changed: 51 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,78 @@
11
export const registerSchema = {
2+
tags: ["Authentication"],
3+
summary: "Register a user",
4+
description: "Creates a user account and returns access and refresh tokens.",
25
body: {
36
type: "object",
47
additionalProperties: false,
58
required: ["email", "password"],
69
properties: {
7-
email: {
8-
type: "string",
9-
format: "email"
10-
},
11-
password: {
12-
type: "string",
13-
minLength: 8
14-
},
15-
name: {
16-
type: "string",
17-
minLength: 1,
18-
maxLength: 100
19-
}
10+
email: { type: "string", format: "email", examples: ["user@example.com"] },
11+
password: { type: "string", minLength: 8, examples: ["strong-password"] },
12+
name: { type: "string", minLength: 1, maxLength: 100, examples: ["Example User"] }
2013
}
14+
},
15+
response: {
16+
201: { $ref: "AuthResponse#" },
17+
409: { $ref: "ErrorResponse#" }
2118
}
2219
} as const;
2320

2421
export const loginSchema = {
22+
tags: ["Authentication"],
23+
summary: "Log in a user",
24+
description: "Authenticates credentials and returns access and refresh tokens.",
2525
body: {
2626
type: "object",
2727
additionalProperties: false,
2828
required: ["email", "password"],
2929
properties: {
30-
email: {
31-
type: "string",
32-
format: "email"
33-
},
34-
password: {
35-
type: "string",
36-
minLength: 1
37-
}
30+
email: { type: "string", format: "email" },
31+
password: { type: "string", minLength: 1 }
3832
}
33+
},
34+
response: {
35+
200: { $ref: "AuthResponse#" },
36+
401: { $ref: "ErrorResponse#" }
3937
}
4038
} as const;
4139

4240
export const refreshSchema = {
41+
tags: ["Authentication"],
42+
summary: "Refresh authentication tokens",
43+
description: "Rotates a valid refresh token and returns a new token pair.",
4344
body: {
4445
type: "object",
4546
additionalProperties: false,
4647
required: ["refreshToken"],
47-
properties: {
48-
refreshToken: {
49-
type: "string",
50-
minLength: 1
51-
}
52-
}
48+
properties: { refreshToken: { type: "string", minLength: 1 } }
49+
},
50+
response: {
51+
200: { $ref: "TokenPair#" },
52+
401: { $ref: "ErrorResponse#" }
53+
}
54+
} as const;
55+
56+
export const logoutSchema = {
57+
tags: ["Authentication"],
58+
summary: "Log out a user",
59+
description: "Revokes the supplied refresh token.",
60+
body: {
61+
type: "object",
62+
additionalProperties: false,
63+
required: ["refreshToken"],
64+
properties: { refreshToken: { type: "string", minLength: 1 } }
65+
},
66+
response: { 204: { type: "null" } }
67+
} as const;
68+
69+
export const meSchema = {
70+
tags: ["Authentication"],
71+
summary: "Get the authenticated user",
72+
description: "Returns the currently authenticated user's public profile.",
73+
security: [{ bearerAuth: [] }],
74+
response: {
75+
200: { $ref: "UserResponse#" },
76+
401: { $ref: "ErrorResponse#" }
5377
}
5478
} as const;

dist/templates/api/src/plugins/index.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import { configPlugin } from "./config";
44
import { corsPlugin } from "./cors";
55
import { databasePlugin } from "./database";
66
import { errorHandlerPlugin } from "./error-handler";
7+
import { schemasPlugin } from "./schemas";
78
import { sensiblePlugin } from "./sensible";
9+
import { swaggerPlugin } from "./swagger";
810

9-
export async function registerPlugins(
10-
app: FastifyInstance
11-
): Promise<void> {
11+
export async function registerPlugins(app: FastifyInstance): Promise<void> {
1212
await app.register(configPlugin);
13+
await app.register(schemasPlugin);
14+
await app.register(swaggerPlugin);
1315
await app.register(databasePlugin);
1416
await app.register(sensiblePlugin);
1517
await app.register(authPlugin);
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import fp from "fastify-plugin";
2+
import { authResponseSchema, tokenPairSchema, userResponseSchema } from "../schemas/auth";
3+
import { databaseHealthResponseSchema, errorResponseSchema, healthResponseSchema } from "../schemas/common";
4+
5+
export const schemasPlugin = fp(
6+
async (app) => {
7+
app.addSchema(errorResponseSchema);
8+
app.addSchema(healthResponseSchema);
9+
app.addSchema(databaseHealthResponseSchema);
10+
app.addSchema(userResponseSchema);
11+
app.addSchema(tokenPairSchema);
12+
app.addSchema(authResponseSchema);
13+
},
14+
{ name: "schemas" }
15+
);
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import swagger from "@fastify/swagger";
2+
import swaggerUi from "@fastify/swagger-ui";
3+
import fp from "fastify-plugin";
4+
5+
export const swaggerPlugin = fp(
6+
async (app) => {
7+
await app.register(swagger, {
8+
openapi: {
9+
info: {
10+
title: "{{PROJECT_DISPLAY_NAME}} API",
11+
description: "API documentation generated by LaunchStack CLI.",
12+
version: "0.1.0"
13+
},
14+
servers: [
15+
{
16+
url: `http://localhost:${app.config.port}`,
17+
description: "Local development"
18+
}
19+
],
20+
tags: [
21+
{ name: "System", description: "Health and operational endpoints" },
22+
{ name: "Authentication", description: "User authentication and token lifecycle" }
23+
],
24+
components: {
25+
securitySchemes: {
26+
bearerAuth: {
27+
type: "http",
28+
scheme: "bearer",
29+
bearerFormat: "JWT"
30+
}
31+
}
32+
}
33+
}
34+
});
35+
36+
await app.register(swaggerUi, {
37+
routePrefix: "/docs",
38+
uiConfig: {
39+
docExpansion: "list",
40+
deepLinking: true,
41+
displayRequestDuration: true
42+
},
43+
staticCSP: true,
44+
transformStaticCSP: (header) => header
45+
});
46+
},
47+
{ name: "swagger", dependencies: ["config"] }
48+
);

0 commit comments

Comments
 (0)