Skip to content
Merged
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
1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"swagger-ui-express": "^5.0.1",
"winston": "^3.19.0",
"ws": "^8.20.0",
"yaml": "^2.0.0",
"y-websocket": "^3.0.0",
"yjs": "^13.6.30",
"zod": "^4.3.6"
Expand Down
73 changes: 73 additions & 0 deletions backend/src/config/swagger.serve.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* swagger.serve.ts — Issue #1128
*
* Serves the OpenAPI 3.1 specification as an interactive Swagger UI at
* `/api/docs`, and exports the machine-readable artifacts at `/api/docs/spec.json`
* and `/api/docs/spec.yaml` for automated SDK generation.
*
* The spec itself is generated by `swagger-jsdoc` from the `@openapi` JSDoc
* blocks across the route files (see ./swagger.ts).
*/

import { Router, type RequestHandler } from 'express';
import swaggerUi from 'swagger-ui-express';
import YAML from 'yaml';
import { swaggerSpec } from './swagger.js';

const router: ReturnType<typeof Router> = Router();

/** Structural view of the OpenAPI document used by the info handler. */
interface OpenApiMeta {
openapi?: string;
info?: { title?: string; version?: string };
paths?: Record<string, unknown>;
components?: { schemas?: Record<string, unknown> };
}

const specMeta = swaggerSpec as OpenApiMeta;
const yamlSpec = YAML.stringify(swaggerSpec);

/** GET /api/docs/spec.json — OpenAPI 3.1 document as JSON. */
export const specJsonHandler: RequestHandler = (_req, res) => {
res.setHeader('Content-Type', 'application/json');
res.status(200).send(JSON.stringify(swaggerSpec, null, 2));
};

/** GET /api/docs/spec.yaml — OpenAPI 3.1 document as YAML. */
export const specYamlHandler: RequestHandler = (_req, res) => {
res.setHeader('Content-Type', 'application/yaml');
res.status(200).send(yamlSpec);
};

/** Health endpoint reporting whether the spec was generated. */
export const docsInfoHandler: RequestHandler = (_req, res) => {
res.json({
status: 'success',
data: {
openapi: specMeta.openapi,
title: specMeta.info?.title,
version: specMeta.info?.version,
paths: Object.keys(specMeta.paths ?? {}).length,
schemas: Object.keys(specMeta.components?.schemas ?? {}).length,
ui: '/api/docs',
json: '/api/docs/spec.json',
yaml: '/api/docs/spec.yaml',
},
});
};

router.get('/spec.json', specJsonHandler);
router.get('/spec.yaml', specYamlHandler);
router.get('/info', docsInfoHandler);
router.use('/', swaggerUi.serve);
router.get(
'/',
swaggerUi.setup(swaggerSpec, {
customSiteTitle: 'Web3 Student Lab API — Swagger UI',
swaggerOptions: {
url: '/api/docs/spec.json',
},
})
);

export default router;
2 changes: 1 addition & 1 deletion backend/src/config/swagger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import swaggerJsdoc from 'swagger-jsdoc';

const options: swaggerJsdoc.Options = {
definition: {
openapi: '3.0.0',
openapi: '3.1.0',
info: {
title: 'Web3 Student Lab API Documentation',
version: '1.0.0',
Expand Down
4 changes: 4 additions & 0 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import routes from './routes/index.js';
import { initializeSentry, getSentryRequestHandler, getSentryErrorHandler } from './utils/sentry.js';
import { jsonBodySizeLimit } from './middleware/bodySizeLimit.js';
import { createCorsMiddleware } from './config/cors.config.js';
import swaggerDocsRouter from './config/swagger.serve.js';
import logger from './utils/logger.js';

dotenv.config();
Expand Down Expand Up @@ -37,6 +38,9 @@ app.post('/api/security/csp-report', express.json(), (req: Request, res: Respons
res.status(204).end();
});

// Mount OpenAPI 3.1 interactive docs (Swagger UI) + JSON/YAML spec export.
app.use('/api/docs', swaggerDocsRouter);

// Mount main API v1 router
app.use('/api/v1', routes);

Expand Down