@@ -13,6 +13,9 @@ import { WebSocketServer } from "ws";
1313import { RPCHandler } from "@orpc/server/node" ;
1414import { RPCHandler as ORPCWebSocketServerHandler } from "@orpc/server/ws" ;
1515import { onError } from "@orpc/server" ;
16+ import { OpenAPIGenerator } from "@orpc/openapi" ;
17+ import { OpenAPIHandler } from "@orpc/openapi/node" ;
18+ import { ZodToJsonSchemaConverter } from "@orpc/zod/zod4" ;
1619import { router , type AppRouter } from "@/node/orpc/router" ;
1720import type { ORPCContext } from "@/node/orpc/context" ;
1821import { extractWsHeaders } from "@/node/orpc/authMiddleware" ;
@@ -53,6 +56,10 @@ export interface OrpcServer {
5356 baseUrl : string ;
5457 /** WebSocket URL for WS connections */
5558 wsUrl : string ;
59+ /** URL for OpenAPI spec JSON */
60+ specUrl : string ;
61+ /** URL for Scalar API docs */
62+ docsUrl : string ;
5663 /** Close the server and cleanup resources */
5764 close : ( ) => Promise < void > ;
5865}
@@ -100,6 +107,77 @@ export async function createOrpcServer({
100107
101108 const orpcRouter = existingRouter ?? router ( authToken ) ;
102109
110+ // OpenAPI generator for spec endpoint
111+ const openAPIGenerator = new OpenAPIGenerator ( {
112+ schemaConverters : [ new ZodToJsonSchemaConverter ( ) ] ,
113+ } ) ;
114+
115+ // OpenAPI spec endpoint
116+ app . get ( "/api/spec.json" , async ( _req , res ) => {
117+ const spec = await openAPIGenerator . generate ( orpcRouter , {
118+ info : {
119+ title : "Mux API" ,
120+ version : VERSION . git_describe ,
121+ description : "API for Mux" ,
122+ } ,
123+ servers : [ { url : "/api" } ] ,
124+ security : authToken ? [ { bearerAuth : [ ] } ] : undefined ,
125+ components : authToken
126+ ? {
127+ securitySchemes : {
128+ bearerAuth : {
129+ type : "http" ,
130+ scheme : "bearer" ,
131+ } ,
132+ } ,
133+ }
134+ : undefined ,
135+ } ) ;
136+ res . json ( spec ) ;
137+ } ) ;
138+
139+ // Scalar API reference UI
140+ app . get ( "/api/docs" , ( _req , res ) => {
141+ const html = `<!doctype html>
142+ <html>
143+ <head>
144+ <title>mux API Reference</title>
145+ <meta charset="utf-8" />
146+ <meta name="viewport" content="width=device-width, initial-scale=1" />
147+ </head>
148+ <body>
149+ <div id="app"></div>
150+ <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
151+ <script>
152+ Scalar.createApiReference('#app', {
153+ url: '/api/spec.json',
154+ ${ authToken ? "authentication: { securitySchemes: { bearerAuth: { token: '' } } }," : "" }
155+ })
156+ </script>
157+ </body>
158+ </html>` ;
159+ res . setHeader ( "Content-Type" , "text/html" ) ;
160+ res . send ( html ) ;
161+ } ) ;
162+
163+ // OpenAPI REST handler (for Scalar/OpenAPI clients)
164+ const openAPIHandler = new OpenAPIHandler ( orpcRouter , {
165+ interceptors : [ onError ( onOrpcError ) ] ,
166+ } ) ;
167+
168+ app . use ( "/api" , async ( req , res , next ) => {
169+ // Skip spec.json and docs routes - they're handled above
170+ if ( req . path === "/spec.json" || req . path === "/docs" ) {
171+ return next ( ) ;
172+ }
173+ const { matched } = await openAPIHandler . handle ( req , res , {
174+ prefix : "/api" ,
175+ context : { ...context , headers : req . headers } ,
176+ } ) ;
177+ if ( matched ) return ;
178+ next ( ) ;
179+ } ) ;
180+
103181 // oRPC HTTP handler
104182 const orpcHandler = new RPCHandler ( orpcRouter , {
105183 interceptors : [ onError ( onOrpcError ) ] ,
@@ -161,6 +239,8 @@ export async function createOrpcServer({
161239 port : actualPort ,
162240 baseUrl : `http://${ connectableHost } :${ actualPort } ` ,
163241 wsUrl : `ws://${ connectableHost } :${ actualPort } /orpc/ws` ,
242+ specUrl : `http://${ connectableHost } :${ actualPort } /api/spec.json` ,
243+ docsUrl : `http://${ connectableHost } :${ actualPort } /api/docs` ,
164244 close : async ( ) => {
165245 // Close WebSocket server first
166246 wsServer . close ( ) ;
0 commit comments