feat(docs): add auto-generated API documentation with Scalar UI

- Install next-openapi-gen for automatic OpenAPI spec generation from routes
- Add /api-docs page with Scalar UI for interactive API documentation
- Add generate step to build script (runs on every deploy)
- Configure to scan Zod schemas and App Router API routes
- Fix migration 0071 to use IF NOT EXISTS for idempotency
- Add public/openapi.json to .gitignore (generated file)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2026-01-15 15:56:27 -06:00
parent 633c789338
commit 31b9f9dac1
6 changed files with 1886 additions and 89 deletions

1
apps/web/.gitignore vendored
View File

@ -51,6 +51,7 @@ styled-system
# generated
src/generated/build-info.json
public/openapi.json
# biome
.biome

View File

@ -1,5 +1,5 @@
-- MCP API Keys for external tool access (Claude Code, etc.)
CREATE TABLE `mcp_api_keys` (
CREATE TABLE IF NOT EXISTS `mcp_api_keys` (
`id` text PRIMARY KEY NOT NULL,
`user_id` text NOT NULL,
`key` text NOT NULL,
@ -9,5 +9,5 @@ CREATE TABLE `mcp_api_keys` (
`created_at` integer NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
);--> statement-breakpoint
CREATE INDEX `mcp_api_keys_user_id_idx` ON `mcp_api_keys` (`user_id`);--> statement-breakpoint
CREATE UNIQUE INDEX `mcp_api_keys_key_idx` ON `mcp_api_keys` (`key`);
CREATE INDEX IF NOT EXISTS `mcp_api_keys_user_id_idx` ON `mcp_api_keys` (`user_id`);--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS `mcp_api_keys_key_idx` ON `mcp_api_keys` (`key`);

114
apps/web/next.openapi.json Normal file
View File

@ -0,0 +1,114 @@
{
"openapi": "3.0.0",
"info": {
"title": "Abaci One API",
"version": "1.0.0",
"description": "API for Abaci One - Soroban abacus learning platform"
},
"servers": [
{
"url": "https://abaci.one/api",
"description": "Production server"
},
{
"url": "http://localhost:3000/api",
"description": "Local development server"
}
],
"components": {
"securitySchemes": {
"BearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
}
}
},
"defaultResponseSet": "common",
"responseSets": {
"common": [
"400",
"500"
],
"auth": [
"400",
"401",
"403",
"500"
],
"public": [
"400",
"500"
]
},
"errorConfig": {
"template": {
"type": "object",
"properties": {
"error": {
"type": "string",
"example": "{{ERROR_MESSAGE}}"
},
"code": {
"type": "string",
"example": "{{ERROR_CODE}}"
}
}
},
"codes": {
"400": {
"description": "Bad Request",
"variables": {
"ERROR_MESSAGE": "Invalid request parameters",
"ERROR_CODE": "BAD_REQUEST"
}
},
"401": {
"description": "Unauthorized",
"variables": {
"ERROR_MESSAGE": "Authentication required",
"ERROR_CODE": "UNAUTHORIZED"
}
},
"403": {
"description": "Forbidden",
"variables": {
"ERROR_MESSAGE": "Access denied",
"ERROR_CODE": "FORBIDDEN"
}
},
"404": {
"description": "Not Found",
"variables": {
"ERROR_MESSAGE": "Resource not found",
"ERROR_CODE": "NOT_FOUND"
}
},
"409": {
"description": "Conflict",
"variables": {
"ERROR_MESSAGE": "Resource already exists",
"ERROR_CODE": "CONFLICT"
}
},
"500": {
"description": "Internal Server Error",
"variables": {
"ERROR_MESSAGE": "An unexpected error occurred",
"ERROR_CODE": "INTERNAL_ERROR"
}
}
}
},
"apiDir": "./src/app/api",
"schemaDir": "./src",
"schemaType": "zod",
"schemaFiles": [],
"docsUrl": "api-docs",
"ui": "scalar",
"outputFile": "openapi.json",
"outputDir": "./public",
"includeOpenApiRoutes": false,
"ignoreRoutes": [],
"debug": false
}

View File

@ -4,7 +4,7 @@
"private": true,
"scripts": {
"dev": "npx @pandacss/dev --clean && tsc -p tsconfig.server.json && tsc-alias -p tsconfig.server.json && concurrently \"node server.js\" \"npx @pandacss/dev --watch --poll\"",
"build": "node scripts/generate-build-info.js && npx tsx scripts/generateAllDayIcons.tsx && npx @pandacss/dev && tsc -p tsconfig.server.json && tsc-alias -p tsconfig.server.json && npm run build:seed-script && next build",
"build": "node scripts/generate-build-info.js && npx next-openapi-gen generate && npx tsx scripts/generateAllDayIcons.tsx && npx @pandacss/dev && tsc -p tsconfig.server.json && tsc-alias -p tsconfig.server.json && npm run build:seed-script && next build",
"start": "NODE_ENV=production node server.js",
"lint": "npx @biomejs/biome lint . && npx eslint .",
"lint:fix": "npx @biomejs/biome lint . --write && npx eslint . --fix",
@ -51,6 +51,7 @@
"@react-spring/web": "^10.0.3",
"@react-three/drei": "^9.117.0",
"@react-three/fiber": "^8.17.0",
"@scalar/api-reference-react": "^0.8.24",
"@socket.io/redis-adapter": "^8.3.0",
"@soroban/abacus-react": "workspace:*",
"@soroban/core": "workspace:*",
@ -69,6 +70,7 @@
"@types/jsdom": "^21.1.7",
"@types/qrcode": "^1.5.6",
"@use-gesture/react": "^10.3.1",
"ajv": "^8.17.1",
"bcryptjs": "^2.4.3",
"better-sqlite3": "^12.4.1",
"canvas-confetti": "^1.9.4",
@ -150,6 +152,7 @@
"eslint-plugin-storybook": "^9.1.7",
"happy-dom": "^18.0.1",
"jsdom": "^27.0.0",
"next-openapi-gen": "^0.9.4",
"sharp": "^0.34.5",
"storybook": "^9.1.7",
"tsc-alias": "^1.8.16",

View File

@ -0,0 +1,16 @@
"use client";
import { ApiReferenceReact } from "@scalar/api-reference-react";
import "@scalar/api-reference-react/style.css";
export default function ApiDocsPage() {
return (
<ApiReferenceReact
configuration={{
_integration: "nextjs",
url: "/openapi.json",
}}
/>
);
}

File diff suppressed because it is too large Load Diff