Skip to content

Commit 7d55920

Browse files
author
Rajat
committed
Add Swagger UI API documentation
1 parent b41d415 commit 7d55920

9 files changed

Lines changed: 74 additions & 2 deletions

File tree

‎bun.lock‎

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎examples/reference-product/README.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ The API listens on `http://127.0.0.1:4000`. `SEED=1` in the example environment
2424
creates local demo users, tenants, credentials, notes, and billing data. Do
2525
not enable it for a production deployment.
2626

27+
Open `http://127.0.0.1:4000/docs` for the interactive Swagger UI, or fetch the
28+
generated OpenAPI document from `http://127.0.0.1:4000/openapi.json`.
29+
2730
In a second terminal, start the Next.js web app:
2831

2932
```bash

‎examples/reference-product/apps/api/package.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,14 @@
4040
"express": "^4.21.2",
4141
"pg": "^8.23.0",
4242
"pino": "^9.6.0",
43+
"swagger-ui-express": "^5.0.1",
4344
"zod": "^3.25.76"
4445
},
4546
"devDependencies": {
4647
"@electric-sql/pglite": "^0.2.17",
4748
"@types/express": "^4.17.21",
4849
"@types/node": "^22.14.1",
50+
"@types/swagger-ui-express": "^4.1.8",
4951
"drizzle-kit": "0.31.4",
5052
"typescript": "^5.9.3",
5153
"@types/bun": "^1.4.1"

‎examples/reference-product/apps/api/src/app.test.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,15 @@ describe.serial("reference API adapters", () => {
473473
const openapi = await fetch(`http://127.0.0.1:${address.port}/openapi.json`);
474474
expect(openapi.status).toBe(200);
475475
expect((await openapi.json()).paths["/v1/notes"]).toBeDefined();
476+
const docs = await fetch(`http://127.0.0.1:${address.port}/docs`);
477+
expect(docs.status).toBe(200);
478+
expect(docs.headers.get("content-type")).toContain("text/html");
479+
expect(await docs.text()).toContain("Swagger UI");
480+
const docsInitializer = await fetch(
481+
`http://127.0.0.1:${address.port}/docs/swagger-ui-init.js`,
482+
);
483+
expect(docsInitializer.status).toBe(200);
484+
expect(await docsInitializer.text()).toContain('"/v1/notes"');
476485
const protectedResource = await fetch(
477486
`http://127.0.0.1:${address.port}/.well-known/oauth-protected-resource/mcp`,
478487
);

‎examples/reference-product/apps/api/src/express-app.ts‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import { readOrCreateRequestId } from "@codelitdev/platform";
44
import { contract } from "@reference-product/api-contract";
55
import { createExpressEndpoints, initServer } from "@ts-rest/express";
66
import { toNodeHandler } from "better-auth/node";
7-
import express, { type Express } from "express";
7+
import express, { type Express, type RequestHandler } from "express";
8+
import swaggerUi from "swagger-ui-express";
89
import { AUTH_BASE_PATH, MCP_SCOPES_SUPPORTED } from "./auth/options.js";
910
import type { DispatchDeps } from "./deps.js";
1011
import { dispatch } from "./dispatch.js";
12+
import { createOpenApiDocument } from "./openapi.js";
1113

1214
export function createExpressApp(deps: DispatchDeps): Express {
1315
const app = express();
@@ -64,6 +66,21 @@ export function createExpressApp(deps: DispatchDeps): Express {
6466
loginMethods: [{ type: "email-otp" }],
6567
}),
6668
);
69+
// The Swagger typings can resolve a separate Express type version in a
70+
// freshly generated workspace, so normalize its handlers at this boundary.
71+
app.use(
72+
"/docs",
73+
...(swaggerUi.serve as unknown as RequestHandler[]),
74+
swaggerUi.setup(createOpenApiDocument(deps.auth.publicApiUrl), {
75+
explorer: true,
76+
swaggerOptions: {
77+
persistAuthorization: true,
78+
displayRequestDuration: true,
79+
docExpansion: "none",
80+
defaultModelsExpandDepth: -1,
81+
},
82+
}) as unknown as RequestHandler,
83+
);
6784
app.use(express.json({ limit: "32kb" }));
6885

6986
const server = initServer();
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
# __PRODUCT_NAME__ API
22

33
Product-owned application services live here.
4+
5+
Open the interactive Swagger UI at `GET /docs`, or fetch the generated OpenAPI
6+
document directly from `GET /openapi.json`.

‎templates/saas-product/apps/api/package.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,14 @@
4040
"express": "^4.21.2",
4141
"pg": "^8.23.0",
4242
"pino": "^9.6.0",
43+
"swagger-ui-express": "^5.0.1",
4344
"zod": "^3.25.76"
4445
},
4546
"devDependencies": {
4647
"@electric-sql/pglite": "^0.2.17",
4748
"@types/express": "^4.17.21",
4849
"@types/node": "^22.14.1",
50+
"@types/swagger-ui-express": "^4.1.8",
4951
"drizzle-kit": "0.31.4",
5052
"typescript": "^5.9.3",
5153
"@types/bun": "^1.4.1"

‎templates/saas-product/apps/api/src/app.test.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,15 @@ describe.serial("reference API adapters", () => {
473473
const openapi = await fetch(`http://127.0.0.1:${address.port}/openapi.json`);
474474
expect(openapi.status).toBe(200);
475475
expect((await openapi.json()).paths["/v1/notes"]).toBeDefined();
476+
const docs = await fetch(`http://127.0.0.1:${address.port}/docs`);
477+
expect(docs.status).toBe(200);
478+
expect(docs.headers.get("content-type")).toContain("text/html");
479+
expect(await docs.text()).toContain("Swagger UI");
480+
const docsInitializer = await fetch(
481+
`http://127.0.0.1:${address.port}/docs/swagger-ui-init.js`,
482+
);
483+
expect(docsInitializer.status).toBe(200);
484+
expect(await docsInitializer.text()).toContain('"/v1/notes"');
476485
const protectedResource = await fetch(
477486
`http://127.0.0.1:${address.port}/.well-known/oauth-protected-resource/mcp`,
478487
);

‎templates/saas-product/apps/api/src/express-app.ts‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import { createMcpOAuthDiscoveryRoutes } from "@codelitdev/oauth-server-kit/mcp"
44
import { readOrCreateRequestId } from "@codelitdev/platform";
55
import { createExpressEndpoints, initServer } from "@ts-rest/express";
66
import { toNodeHandler } from "better-auth/node";
7-
import express, { type Express } from "express";
7+
import express, { type Express, type RequestHandler } from "express";
8+
import swaggerUi from "swagger-ui-express";
89
import { AUTH_BASE_PATH, MCP_SCOPES_SUPPORTED } from "./auth/options.js";
910
import type { DispatchDeps } from "./deps.js";
1011
import { dispatch } from "./dispatch.js";
12+
import { createOpenApiDocument } from "./openapi.js";
1113

1214
export function createExpressApp(deps: DispatchDeps): Express {
1315
const app = express();
@@ -64,6 +66,21 @@ export function createExpressApp(deps: DispatchDeps): Express {
6466
loginMethods: [{ type: "email-otp" }],
6567
}),
6668
);
69+
// The Swagger typings can resolve a separate Express type version in a
70+
// freshly generated workspace, so normalize its handlers at this boundary.
71+
app.use(
72+
"/docs",
73+
...(swaggerUi.serve as unknown as RequestHandler[]),
74+
swaggerUi.setup(createOpenApiDocument(deps.auth.publicApiUrl), {
75+
explorer: true,
76+
swaggerOptions: {
77+
persistAuthorization: true,
78+
displayRequestDuration: true,
79+
docExpansion: "none",
80+
defaultModelsExpandDepth: -1,
81+
},
82+
}) as unknown as RequestHandler,
83+
);
6784
app.use(express.json({ limit: "32kb" }));
6885

6986
const server = initServer();

0 commit comments

Comments
 (0)