feat(cli): add openapi suppport in orpc (#563)

This commit is contained in:
Aman Varshney
2025-09-04 15:08:44 +05:30
committed by GitHub
parent 50a1b9441f
commit a276addef8
15 changed files with 193 additions and 24 deletions

View File

@@ -112,6 +112,8 @@ export const dependencyVersionMap = {
"@orpc/server": "^1.8.6", "@orpc/server": "^1.8.6",
"@orpc/client": "^1.8.6", "@orpc/client": "^1.8.6",
"@orpc/openapi": "^1.8.6",
"@orpc/zod": "^1.8.6",
"@orpc/tanstack-query": "^1.8.6", "@orpc/tanstack-query": "^1.8.6",
"@trpc/tanstack-react-query": "^11.5.0", "@trpc/tanstack-react-query": "^11.5.0",

View File

@@ -54,7 +54,14 @@ function getApiDependencies(
> = {}; > = {};
if (api === "orpc") { if (api === "orpc") {
deps.server = { dependencies: ["@orpc/server", "@orpc/client"] }; deps.server = {
dependencies: [
"@orpc/server",
"@orpc/client",
"@orpc/openapi",
"@orpc/zod",
],
};
} else if (api === "trpc") { } else if (api === "trpc") {
deps.server = { dependencies: ["@trpc/server", "@trpc/client"] }; deps.server = { dependencies: ["@trpc/server", "@trpc/client"] };
} }

View File

@@ -14,6 +14,7 @@ export async function displayPostInstallInstructions(
config: ProjectConfig & { depsInstalled: boolean }, config: ProjectConfig & { depsInstalled: boolean },
) { ) {
const { const {
api,
database, database,
relativePath, relativePath,
packageManager, packageManager,
@@ -158,6 +159,14 @@ export async function displayPostInstallInstructions(
if (!isConvex) { if (!isConvex) {
output += `${pc.cyan("•")} Backend API: http://localhost:3000\n`; output += `${pc.cyan("•")} Backend API: http://localhost:3000\n`;
if (api === "orpc") {
if (backend === "next") {
output += `${pc.cyan("•")} OpenAPI (Scalar UI): http://localhost:3000/rpc/api\n`;
} else {
output += `${pc.cyan("•")} OpenAPI (Scalar UI): http://localhost:3000/api\n`;
}
}
} }
if (addons?.includes("starlight")) { if (addons?.includes("starlight")) {

View File

@@ -25,7 +25,6 @@ export async function setupNextAlchemyDeploy(
...pkg.scripts, ...pkg.scripts,
deploy: "alchemy deploy", deploy: "alchemy deploy",
destroy: "alchemy destroy", destroy: "alchemy destroy",
dev: "alchemy dev",
}; };
} }
await fs.writeJson(pkgPath, pkg, { spaces: 2 }); await fs.writeJson(pkgPath, pkg, { spaces: 2 });

View File

@@ -26,7 +26,6 @@ export async function setupNuxtAlchemyDeploy(
...pkg.scripts, ...pkg.scripts,
deploy: "alchemy deploy", deploy: "alchemy deploy",
destroy: "alchemy destroy", destroy: "alchemy destroy",
dev: "alchemy dev",
}; };
} }
await fs.writeJson(pkgPath, pkg, { spaces: 2 }); await fs.writeJson(pkgPath, pkg, { spaces: 2 });

View File

@@ -25,7 +25,6 @@ export async function setupReactRouterAlchemyDeploy(
...pkg.scripts, ...pkg.scripts,
deploy: "alchemy deploy", deploy: "alchemy deploy",
destroy: "alchemy destroy", destroy: "alchemy destroy",
dev: "alchemy dev",
}; };
} }
await fs.writeJson(pkgPath, pkg, { spaces: 2 }); await fs.writeJson(pkgPath, pkg, { spaces: 2 });

View File

@@ -25,7 +25,6 @@ export async function setupSolidAlchemyDeploy(
...pkg.scripts, ...pkg.scripts,
deploy: "alchemy deploy", deploy: "alchemy deploy",
destroy: "alchemy destroy", destroy: "alchemy destroy",
dev: "alchemy dev",
}; };
} }
await fs.writeJson(pkgPath, pkg, { spaces: 2 }); await fs.writeJson(pkgPath, pkg, { spaces: 2 });

View File

@@ -26,7 +26,6 @@ export async function setupSvelteAlchemyDeploy(
...pkg.scripts, ...pkg.scripts,
deploy: "alchemy deploy", deploy: "alchemy deploy",
destroy: "alchemy destroy", destroy: "alchemy destroy",
dev: "alchemy dev",
}; };
} }

View File

@@ -25,7 +25,6 @@ export async function setupTanStackRouterAlchemyDeploy(
...pkg.scripts, ...pkg.scripts,
deploy: "alchemy deploy", deploy: "alchemy deploy",
destroy: "alchemy destroy", destroy: "alchemy destroy",
dev: "alchemy dev",
}; };
} }

View File

@@ -26,7 +26,6 @@ export async function setupTanStackStartAlchemyDeploy(
...pkg.scripts, ...pkg.scripts,
deploy: "alchemy deploy", deploy: "alchemy deploy",
destroy: "alchemy destroy", destroy: "alchemy destroy",
dev: "alchemy dev",
}; };
} }

View File

@@ -2,18 +2,47 @@
import { createContext } from '@/lib/context' import { createContext } from '@/lib/context'
{{/if}} {{/if}}
import { appRouter } from '@/routers' import { appRouter } from '@/routers'
import { OpenAPIHandler } from '@orpc/openapi/fetch'
import { OpenAPIReferencePlugin } from '@orpc/openapi/plugins'
import { ZodToJsonSchemaConverter } from '@orpc/zod/zod4'
import { RPCHandler } from '@orpc/server/fetch' import { RPCHandler } from '@orpc/server/fetch'
import { onError } from '@orpc/server'
import { NextRequest } from 'next/server' import { NextRequest } from 'next/server'
const handler = new RPCHandler(appRouter) const rpcHandler = new RPCHandler(appRouter, {
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
const apiHandler = new OpenAPIHandler(appRouter, {
plugins: [
new OpenAPIReferencePlugin({
schemaConverters: [new ZodToJsonSchemaConverter()],
}),
],
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
async function handleRequest(req: NextRequest) { async function handleRequest(req: NextRequest) {
const { response } = await handler.handle(req, { const rpcResult = await rpcHandler.handle(req, {
prefix: '/rpc', prefix: '/rpc',
context: {{#if (eq auth "better-auth")}}await createContext(req){{else}}{}{{/if}}, context: {{#if (eq auth "better-auth")}}await createContext(req){{else}}{}{{/if}},
}) })
if (rpcResult.response) return rpcResult.response
return response ?? new Response('Not found', { status: 404 }) const apiResult = await apiHandler.handle(req, {
prefix: '/rpc/api',
context: {{#if (eq auth "better-auth")}}await createContext(req){{else}}{}{{/if}},
})
if (apiResult.response) return apiResult.response
return new Response('Not found', { status: 404 })
} }
export const GET = handleRequest export const GET = handleRequest

View File

@@ -10,7 +10,11 @@ import { appRouter } from "./routers/index";
import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
{{/if}} {{/if}}
{{#if (eq api "orpc")}} {{#if (eq api "orpc")}}
import { OpenAPIHandler } from "@orpc/openapi/fetch";
import { OpenAPIReferencePlugin } from "@orpc/openapi/plugins";
import { ZodToJsonSchemaConverter } from "@orpc/zod/zod4";
import { RPCHandler } from "@orpc/server/fetch"; import { RPCHandler } from "@orpc/server/fetch";
import { onError } from "@orpc/server";
import { appRouter } from "./routers"; import { appRouter } from "./routers";
import { createContext } from "./lib/context"; import { createContext } from "./lib/context";
{{/if}} {{/if}}
@@ -19,7 +23,25 @@ import { auth } from "./lib/auth";
{{/if}} {{/if}}
{{#if (eq api "orpc")}} {{#if (eq api "orpc")}}
const handler = new RPCHandler(appRouter); const rpcHandler = new RPCHandler(appRouter, {
interceptors: [
onError((error) => {
console.error(error);
}),
],
});
const apiHandler = new OpenAPIHandler(appRouter, {
plugins: [
new OpenAPIReferencePlugin({
schemaConverters: [new ZodToJsonSchemaConverter()],
}),
],
interceptors: [
onError((error) => {
console.error(error);
}),
],
});
{{/if}} {{/if}}
{{#if (eq runtime "node")}} {{#if (eq runtime "node")}}
@@ -48,12 +70,19 @@ const app = new Elysia()
{{/if}} {{/if}}
{{#if (eq api "orpc")}} {{#if (eq api "orpc")}}
.all('/rpc*', async (context) => { .all('/rpc*', async (context) => {
const { response } = await handler.handle(context.request, { const { response } = await rpcHandler.handle(context.request, {
prefix: '/rpc', prefix: '/rpc',
context: await createContext({ context }) context: await createContext({ context })
}) })
return response ?? new Response('Not Found', { status: 404 }) return response ?? new Response('Not Found', { status: 404 })
}) })
.all('/api*', async (context) => {
const { response } = await apiHandler.handle(context.request, {
prefix: '/api',
context: await createContext({ context })
})
return response ?? new Response('Not Found', { status: 404 })
})
{{/if}} {{/if}}
{{#if (eq api "trpc")}} {{#if (eq api "trpc")}}
.all("/trpc/*", async (context) => { .all("/trpc/*", async (context) => {

View File

@@ -5,7 +5,11 @@ import { createContext } from "./lib/context";
import { appRouter } from "./routers/index"; import { appRouter } from "./routers/index";
{{/if}} {{/if}}
{{#if (eq api "orpc")}} {{#if (eq api "orpc")}}
import { OpenAPIHandler } from "@orpc/openapi/node";
import { OpenAPIReferencePlugin } from "@orpc/openapi/plugins";
import { ZodToJsonSchemaConverter } from "@orpc/zod/zod4";
import { RPCHandler } from "@orpc/server/node"; import { RPCHandler } from "@orpc/server/node";
import { onError } from "@orpc/server";
import { appRouter } from "./routers"; import { appRouter } from "./routers";
{{#if (eq auth "better-auth")}} {{#if (eq auth "better-auth")}}
import { createContext } from "./lib/context"; import { createContext } from "./lib/context";
@@ -50,9 +54,28 @@ app.use(
{{/if}} {{/if}}
{{#if (eq api "orpc")}} {{#if (eq api "orpc")}}
const handler = new RPCHandler(appRouter); const rpcHandler = new RPCHandler(appRouter, {
app.use("/rpc{*path}", async (req, res, next) => { interceptors: [
const { matched } = await handler.handle(req, res, { onError((error) => {
console.error(error);
}),
],
});
const apiHandler = new OpenAPIHandler(appRouter, {
plugins: [
new OpenAPIReferencePlugin({
schemaConverters: [new ZodToJsonSchemaConverter()],
}),
],
interceptors: [
onError((error) => {
console.error(error);
}),
],
});
app.use(async (req, res, next) => {
const rpcResult = await rpcHandler.handle(req, res, {
prefix: "/rpc", prefix: "/rpc",
{{#if (eq auth "better-auth")}} {{#if (eq auth "better-auth")}}
context: await createContext({ req }), context: await createContext({ req }),
@@ -60,7 +83,18 @@ app.use("/rpc{*path}", async (req, res, next) => {
context: {}, context: {},
{{/if}} {{/if}}
}); });
if (matched) return; if (rpcResult.matched) return;
const apiResult = await apiHandler.handle(req, res, {
prefix: "/api",
{{#if (eq auth "better-auth")}}
context: await createContext({ req }),
{{else}}
context: {},
{{/if}}
});
if (apiResult.matched) return;
next(); next();
}); });
{{/if}} {{/if}}

View File

@@ -9,8 +9,12 @@ import { appRouter, type AppRouter } from "./routers/index";
{{/if}} {{/if}}
{{#if (eq api "orpc")}} {{#if (eq api "orpc")}}
import { OpenAPIHandler } from "@orpc/openapi/node";
import { OpenAPIReferencePlugin } from "@orpc/openapi/plugins";
import { ZodToJsonSchemaConverter } from "@orpc/zod/zod4";
import { RPCHandler } from "@orpc/server/node"; import { RPCHandler } from "@orpc/server/node";
import { CORSPlugin } from "@orpc/server/plugins"; import { CORSPlugin } from "@orpc/server/plugins";
import { onError } from "@orpc/server";
import { appRouter } from "./routers/index"; import { appRouter } from "./routers/index";
import { createServer } from "node:http"; import { createServer } from "node:http";
{{#if (eq auth "better-auth")}} {{#if (eq auth "better-auth")}}
@@ -40,7 +44,7 @@ const baseCorsConfig = {
}; };
{{#if (eq api "orpc")}} {{#if (eq api "orpc")}}
const handler = new RPCHandler(appRouter, { const rpcHandler = new RPCHandler(appRouter, {
plugins: [ plugins: [
new CORSPlugin({ new CORSPlugin({
origin: process.env.CORS_ORIGIN, origin: process.env.CORS_ORIGIN,
@@ -48,13 +52,31 @@ const handler = new RPCHandler(appRouter, {
allowHeaders: ["Content-Type", "Authorization"], allowHeaders: ["Content-Type", "Authorization"],
}), }),
], ],
interceptors: [
onError((error) => {
console.error(error);
}),
],
});
const apiHandler = new OpenAPIHandler(appRouter, {
plugins: [
new OpenAPIReferencePlugin({
schemaConverters: [new ZodToJsonSchemaConverter()],
}),
],
interceptors: [
onError((error) => {
console.error(error);
}),
],
}); });
const fastify = Fastify({ const fastify = Fastify({
logger: true, logger: true,
serverFactory: (fastifyHandler) => { serverFactory: (fastifyHandler) => {
const server = createServer(async (req, res) => { const server = createServer(async (req, res) => {
const { matched } = await handler.handle(req, res, { const { matched } = await rpcHandler.handle(req, res, {
context: await createContext(req.headers), context: await createContext(req.headers),
prefix: "/rpc", prefix: "/rpc",
}); });
@@ -63,6 +85,15 @@ const fastify = Fastify({
return; return;
} }
const apiResult = await apiHandler.handle(req, res, {
context: await createContext(req.headers),
prefix: "/api",
});
if (apiResult.matched) {
return;
}
fastifyHandler(req, res); fastifyHandler(req, res);
}); });

View File

@@ -5,7 +5,11 @@ import "dotenv/config";
import { env } from "cloudflare:workers"; import { env } from "cloudflare:workers";
{{/if}} {{/if}}
{{#if (eq api "orpc")}} {{#if (eq api "orpc")}}
import { OpenAPIHandler } from "@orpc/openapi/fetch";
import { OpenAPIReferencePlugin } from "@orpc/openapi/plugins";
import { ZodToJsonSchemaConverter } from "@orpc/zod/zod4";
import { RPCHandler } from "@orpc/server/fetch"; import { RPCHandler } from "@orpc/server/fetch";
import { onError } from "@orpc/server";
import { createContext } from "./lib/context"; import { createContext } from "./lib/context";
import { appRouter } from "./routers/index"; import { appRouter } from "./routers/index";
{{/if}} {{/if}}
@@ -54,17 +58,48 @@ app.on(["POST", "GET"], "/api/auth/**", (c) => auth.handler(c.req.raw));
{{/if}} {{/if}}
{{#if (eq api "orpc")}} {{#if (eq api "orpc")}}
const handler = new RPCHandler(appRouter); export const apiHandler = new OpenAPIHandler(appRouter, {
app.use("/rpc/*", async (c, next) => { plugins: [
new OpenAPIReferencePlugin({
schemaConverters: [new ZodToJsonSchemaConverter()],
}),
],
interceptors: [
onError((error) => {
console.error(error);
}),
],
});
export const rpcHandler = new RPCHandler(appRouter, {
interceptors: [
onError((error) => {
console.error(error);
}),
],
});
app.use("/*", async (c, next) => {
const context = await createContext({ context: c }); const context = await createContext({ context: c });
const { matched, response } = await handler.handle(c.req.raw, {
const rpcResult = await rpcHandler.handle(c.req.raw, {
prefix: "/rpc", prefix: "/rpc",
context: context, context: context,
}); });
if (matched) { if (rpcResult.matched) {
return c.newResponse(response.body, response); return c.newResponse(rpcResult.response.body, rpcResult.response);
} }
const apiResult = await apiHandler.handle(c.req.raw, {
prefix: "/api",
context: context,
});
if (apiResult.matched) {
return c.newResponse(apiResult.response.body, apiResult.response);
}
await next(); await next();
}); });
{{/if}} {{/if}}