jlalmes / trpc-openapi Goto Github PK
View Code? Open in Web Editor NEWOpenAPI support for tRPC 🧩
Home Page: https://www.npmjs.com/package/trpc-openapi
License: MIT License
OpenAPI support for tRPC 🧩
Home Page: https://www.npmjs.com/package/trpc-openapi
License: MIT License
Not particularly the cleanest implementation, but I got an adapter working for Fastify.
https://github.com/SeanCassiere/fastify-trpc-openapi-adapter/blob/master/src/fastify.ts.
Let me know what you think. It's definitely a bit jank since the types
for Fastify's request
and reply
objects don't exactly overlap all the types in NodeHTTPRequest & NodeHTTPResponse
.
First, I'm loving the project so far and I really appreciate all the work you've done already.
I honestly don't mind the output requirement on a query/mutation, however, it would be nice to be able to define Model Schema's and reference those for the outputs inside the docs. (ex. Pet Model from Redoc).
on compile there are errors about input schemas
TRPCError: [mutation.auth.register] - Input parser expects a Zod validator
also, the line app.use('/api', createOpenApiExpressMiddleware({ router: appRouter, createContext }));
produces this error
TS2345: Argument of type '{ router: CreateRouterInner<RootConfig<{ ctx: Context; meta: OpenApiMeta<TRPCMeta>; errorShape: DefaultErrorShape; transformer: DefaultDataTransformer; }>, { ...; }>; createContext: ({ req, res, }: CreateExpressContextOptions) => Promise<...>; }' is not assignable to parameter of type 'CreateOpenApiExpressMiddlewareOptions<Router<TContext, TContext, OpenApiMeta<TMeta>, any, any, any, DefaultErrorShape>>'. Type '{ router: CreateRouterInner<RootConfig<{ ctx: Context; meta: OpenApiMeta<TRPCMeta>; errorShape: DefaultErrorShape; transformer: DefaultDataTransformer; }>, { ...; }>; createContext: ({ req, res, }: CreateExpressContextOptions) => Promise<...>; }' is missing the following properties from type 'CreateOpenApiExpressMiddlewareOptions<Router<TContext, TContext, OpenApiMeta<TMeta>, any, any, any, DefaultErrorShape>>': responseMeta, onError, teardown, maxBodySize
Hi! I got following error when using zod with refine effect as input.
[mutation.okInput] - Input parser must be a ZodObject
Reproducible example.
const appRouter = trpc.router<any, OpenApiMeta>().mutation('okInput', {
meta: { openapi: { enabled: true, path: '/ok-input', method: 'POST' } },
input: z
.object({
a: z.string(),
b: z.string(),
})
.refine((data) => data.a === data.b),
output: z.null(),
resolve: () => null,
});
const openApiDocument = generateOpenApiDocument(appRouter, {
title: 'tRPC OpenAPI',
version: '1.0.0',
baseUrl: 'http://localhost:3000/api',
});
Currently z.preprocess throws the following error Input parser must be a ZodObject
:
{
input: z.preprocess(
camelize,
z.object({
createdAt: z.string(),
deliveryInfo: z.object({
currentRetry: z.number(),
maxRetries: z.number(),
}),
event: z.object({
data: z.object({
new: data,
old: data.nullish(),
}),
op: z.string(),
sessionVariables: z.record(z.string().regex(/x-hasura-.*/), z.string()),
traceContext: z.object({ spanId: z.string(), traceId: z.string() }),
}),
id: z.string(),
table: z.object({ name: z.string(), schema: z.string() }),
trigger: z.object({ name: z.string() }),
}),
)
}
I was trying out the package and noticed that each endpoint is limited to a single tag.
Please see below.
const testRouter = createRouter().query("hello", {
meta: { openapi: { enabled: true, method: "GET", path: "/hello", tag: "OnlyASingleTag" } },
input: z.object({ /** input stuff */ }),
output: z.object({ /** output stuff */ }),
async resolve({ input }) {
/** implementation stuff */
},
});
Maybe this needs to change? Since tags are used to group operations together and a single one could be in two groups. Also, Swagger Docs on the OpenAPI Spec for 3.0.3, states that the tag field should container an Array of string tags. -> string[]
https://swagger.io/specification/#operation-object
Hi James,
Is the tRPC alpha version 10 already supported? If not, when would it be possible?
Also since this package is so closely tied and useful to tRPC, has it been considered to be merged into the tRPC monorepo?
Added webhook to the #feed
channel.
Awesome work on this package. I am getting this weird issue, take a look.
I am trying to take out for example here postsProtectedRouter to its own file. But typescript is yelling at me.
export const postsProtectedRouter = createProtectedRouter()
^
TypeError: (0 , index_1.createProtectedRouter) is not a function
v0
-> v1
migration guide. (currently maintained here #143)v10
. (#73)meta.tag
(use meta.tags
instead). (#92)enabled: true
when meta.openapi
is defined. (#68)$refs
. (#37)method: 'DELETE'
in mutation
procedures. (#123)meta.headers
. (#113)z.refine()
. (#102)z.number()
query input. (#44)z.boolean()
query input. (#44)z.date()
query input. (#44)z.array()
query inputs. (modtree/modtree#358 (comment))Fastify
adapter. (#87)Lambda
adapter. (#115)meta.examples
.override
object to OpenApiMeta
and GenerateOpenApiDocumentOptions
.z.object()
input.
z.object()
if using pathParameters
.?input=
.output
schema requirement. (#61 (comment))zod
validator requirement.Right now it's not possible to have z.number()
's in your input schema for GET requests.
That makes sense since query params are strings (so you get an error like Input parser key: "limit" must be ZodString
).
But often (I'm thinking especially of pagination), you need numbers. Right now, you can recreate similar functionality with z.string().transform((s) => parseInt(s)
, but it's not ideal since you lose, for example, Zod's built-in min/max validation.
At a minimum, this deserves a mention in the documentation. If we're feeling more ambitious, we can introduce autoconversions to number
/date
for common types.
I'm happy to open a PR, but first wanted to hear what you think, @jlalmes.
First thanks for the great work,
When I generate an open api document. is there a way to add documentation for the input,
E.g
input: z.object({
name: z.string().openapi({
example: 'John Doe',
}),
Thanks
Consider the following code from Usage with Express
getUser: t.procedure.input(z.string()).query((req) => {
req.input; // string
return { id: req.input, name: 'Bilbo' };
}),
After adding this line
.meta({ openapi: { method: 'GET', path: '/getUser' } })
trpc-openapi fail to process it
TRPCError: [query.getUser] - Input parser must be a ZodObject
A very interesting library!
We are using zod-to-openapi
and there is a extension called .openapi()
that extends zod and defines the description and example. I wonder how we could do it here so that it's included in the OpenAPI generated spec.
https://github.com/asteasolutions/zod-to-openapi#the-openapi-method
Does this library allow defining parameter descriptions for the input
object?
In TRPC I only need to use zod (or yup) for the input, but the output type gets automatically passed on to the frontend without a need for zod.
I want to make an endpoint for a function that already has a typed response. I would rather not have to rewrite all types into zod.
Defining a query/mutation with no input
results in
TRPCError: [query.<queryName>] - Input parser expects a Zod validator
It's possible to workaround this by defining an empty input:
input: z.object({}),
However, that way an empty object is expected when calling the query via TRPC client.
It's a legit use-case to have queries/mutations with no input
, right? Can we allow omitting input
in such cases?
Hey @jlalmes, this is a great little library!
I am exploring possibilities and as far as I can understand, there is currently no way to define a path
parameter.
Consider the petstore swagger example, the uploadFile
has petId
parameter, which is part of the path.
Could we try to extract the path params by looking for curly braces in meta.openapi.path
, and then find param with the same name in input
and mark that param as from: "path"
instead of from: "query"
?
WIP attempt to implement the above approach: v0.1.0...dodas:feat/path-parameters
Have you thought about supporting this use case?
I am happy to discuss this and potentially take a stab at implementing it.
Thanks!
I would like to use trpc-openapi with Cloudflare Workers.
We currently support adapters for Express, Next.js, Serverless & node:http
When trying to use the trpc-openapi with express example project you will get an error saying that the script for building the project is missing.
And when looking into the package.json it seems to be missing:
trpc-openapi/examples/with-express/package.json
Lines 5 to 7 in 6ffa9f5
When running NPM run dev and npm run dev -w with-express you will get the following error:
After installing the TRPC-openapi it will throw the following error:
Can someone who knows how to run this project update the READ.ME of the with express example?
I suspect that I am missing a step that is necessary for running the example project.
https://github.com/jlalmes/trpc-openapi/blob/master/examples/with-express/README.md
Please note this should be done in the next
branch.
Is your tool intented to be used as just an API endpoint? If so, would it be possible to reduce the meta openapi complexity, and just enable openapi for all routes?
openapi: {
enabled: true, //default from global settings
method: "POST", //default GET for query and POST for mutation
path: "/auth/login", //defaults to TRPC route
tag: "auth",
summary: "Login as an existing user",
},
Is there a URL where one can get the JSON/YAML?
Output validation is required by trpc-openapi
to generate the valid response schema, but then maybe we don't really want to enable runtime output validation for our procedures.
I'm not sure what would be the ideal solution here.
Is there any way to ignore the runtime output validation?
hi!
i need to satisfy a REST call which i can't influence. is there a way to return the response without the wrapper:
instead of this:
{
"ok": true,
"data": "This is good" /* Output from tRPC procedure */
}
return this:
{
"name": "trpc", /* Output from tRPC procedure */
"lang": "TS" /* Output from tRPC procedure */
}
if not, it would be a nice enhancement since i'm going to be not the only one with this issue. i'm probably going to fork this for now until i'm confident enough to make a PR.
const successResponseObject = {
description: 'Successful response',
content: {
'application/json': {
schema: zodSchemaToOpenApiSchemaObject(
z.object({
ok: z.literal(true),
data: schema,
}),
),
},
},
};
```
I think to make adoption easier meta.openapi.method
and meta.openapi.method
can be optional:
meta.openapi.method
is GET for query and POST for mutation. Making deletion with POST is not orthodox but its better to make it optionalmeta.openapi.path
can be the mutation/query name. I prefer /say-hello
to /sayHello
too, but having the former as an option also makes adoption easier.Thoughts?
It would be great to add the ability to define headers https://swagger.io/docs/specification/describing-parameters/#header-parameters.
Hi thanks for the awesome package!
I am still a beginner using tRPC and am using this package to incrementally add a trpc layer into my projects.
I was wondering if there is a possibility of using compression algos such as gzip to the body response.
I understand that data transformers are not supported, but is this out of scope due to how trpc would compress as discussed here?
It seems like something that isn't really applicable to normal tRPC
Not sure what's exactly is going on, but when using trpc-openapi
with a procedure that has input: z.void()
(or z.undefined()
, or z.never()
), I am no longer able to use this procedure via TRPC Client, it fails with following error:
[
{
"code": "invalid_type",
"expected": "object",
"received": "undefined",
"path": [],
"message": "Required"
}
]
Just setting openapi.enabled
to false (while keeping input: z.void()
) fixes this and I am able to use the given procedure with no input.
Enabling openapi for given prodedure breaks usage with TRPC Client.
Can we support ZodEnum
and ZodNativeEnum
types as input
params?
Both of these accept string
as their input, so they should be compatible, right?
It currently throws with
TRPCError: [<procedureName>] - Input parser key: "<propName>" must be a ZodString
I'm currently trying to set this up with AWS Lambda do you think the node:http adaptor would be enough to set this up?
As of now trpc-openapi does not support sending a response using other Content-Types other than "application/json" is there any workaround for that? or i did something wrong here.
Currently, using path params in a mutation results in a openAPI schema where the given param is expected in both requestBody
and parameters
. Instead, I would expect the given param to be omitted from the request body schema, since it's passed via path.
For example, in the method below I would expect blueprintId
to be passed via path and Request body only containing credentialId
.
Will unblock #186
Hey thanks for this library,
I am trying to understand why the value of an input objects must be a ZodString. I get following error : Input parser key: "from" must be ZodString
input: z.object({
from: z.number(),
to: z.number(),
}),
I read here https://github.com/jlalmes/trpc-openapi/blob/next/README.md that the input object is defined as ZodObject<{ [string]: ZodString }>
but openApiSpec also allows number/ integer etc
at least number would be good.
Hi there,
Thank you for taking the time to put this library together! I just came across an issue when opening the swagger UI - I see errors like so:
Could not resolve reference: #/properties/data/properties/id
From looking through the source, it looks like the zod-to-json-schema
refStrategy (root is the default) is not mapping correctly. Maybe because the entire object is not being passed into the function?
Hello,
First, thanks for your library!
I use trpc with Prisma and I have a type problem when I define the output. I don't know if it is a problem from trpc-openapi
my schema.prisma
...
model User {
id String @id @default(cuid()) @map("_id")
name String?
email String? @unique
emailVerified DateTime?
image String?
}
my users.ts router
import { createRouter } from "@server/router/context";
import { z } from "zod";
export const usersRouter = createRouter()
.query("getAll", {
meta: {
openapi: {
enabled: true,
method: 'GET',
path: '/users',
tag: 'users',
summary: 'Read all users'
}
},
input: z.void(),
output: z.object({
id: z.string()
}),
async resolve ({ctx}) {
return await ctx.prisma.user.findMany()
}
});
If someone have an idea, how to resolve my issue?
"message": "The inferred type of 'default' cannot be named without a reference to '.pnpm/[email protected]_beenoklgwfttvph5dgxj7na7aq/node_modules/next/types'. This is likely not portable. A type annotation is necessary.",
Sorry for spamming btw!
./src/pages/api/[...trpc].ts:6:1
Type error: The inferred type of 'default' cannot be named without a reference to '.pnpm/[email protected]_beenoklgwfttvph5dgxj7na7aq/node_modules/next'. This is likely not portable. A type annotation is necessary.
4 |
5 | // Handle incoming OpenAPI requests
6 | export default createOpenApiNextHandler({
| ^
7 | router: appRouter,
8 | createContext,
9 | });
I am getting the following error: TRPCError: Input parser must be a ZodObject
I have looked through all the previous issues and can't seem to figure out what I am doing wrong. I have one route
const createRouter = () => trpc.router<Context, OpenApiMeta>();
export const appRouter= createRouter().query("hello", {
meta: { openapi: { enabled: true, method: "GET", path: "/hello" } },
input: z
.object({
text: z.string().nullish(),
})
.nullish(),
output: z.object({ greeting: z.string() }),
resolve({ input }) {
return {
greeting: `Hello ${input?.text ?? "world"}`,
};
},
});
and as you can see I do have zod as the input parser.
This rule seems odd, as any query parameter is nullish by nature. We would need this to have better typing for the resolver.
Willing to contribute!
On both examples with-nextjs and with-express I'm getting this error.
Anyway: Super cool project!
error - Error: Cannot find module 'tslib'
Require stack:
- /home/martin/node_modules/.pnpm/[email protected]_5zkoywanuxf5zpyqjjt34qanl4/node_modules/trpc-openapi/dist/adapters/index.js
- /home/martin/node_modules/.pnpm/[email protected]_5zkoywanuxf5zpyqjjt34qanl4/node_modules/trpc-openapi/dist/index.js
- /home/martin/workspace/api-repo/with-nextjs/.next/server/pages/api/openapi.json.js
Open directory with with-nextjs:
Hey Guys
I've started using the T3 Stack with trpc / nextjs and wanted to use this awesome package to my Project.
Everything works fine and I see the OpenAPI-Docs in the expected route. But i dont understand why this happens
When looking at my trpc query, I have the following:
export const exampleRouter = createRouter().query("hello", {
meta: { openapi: { enabled: true, method: "GET", path: "/api/trpc/example.hello" } },
input: z.object({ name: z.string() }),
output: z.object({ greeting: z.string() }),
resolve: ({ input }) => {
return { greeting: `Hello ${input.name}!` };
},
});
which generated the OpenAPI docs but when i try to execute it throught the UI it generates the URL like this:
http://0.0.0.0:3000/api/trpc/example.hello?name=asdf
which is of course not wrong. But when looking into the dev-tools on my page the request generated from trpc-client is generated like this:
http://localhost:3000/api/trpc/example.hello?batch=1&input={"0":{"json":{"name":"jerome"}}}
Can i change this behavior for the generation of the OpenAPI docs or how would you recommend trying to fix this?
Thanks in advanced
Jerome
Consider the following code
class User {
name: string;
constructor(name: string) {
this.name = name;
}
}
const user = new User('John');
export const trpcRouter = t.router({
getUser: t.procedure
.input(
z.object({
userId: z.number(),
}),
)
.output(z.instanceof(User))
.meta({ openapi: { method: 'POST', path: '/user' } })
.mutation(() => user),
});
Expected output schema
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
},
"required": [
"name"
],
"additionalProperties": false
}
What we actually have
"schema": {}
Hi,
I am getting the following error: TRPCError: Input parser must be a ZodObject. I am following the nextjs example provided in trpc-openapi/src/examples. Getting the same error on all trpc-open api routes [...trpc] including the openapi.json route.
I have looked through all the previous issues and can't seem to figure out what I am doing wrong.
Please let me know if additional details are required.
Regards,
Dawood
Package.json:
{
"name": "next-boilerplate",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@tanstack/react-query": "^4.20.2",
"@trpc/client": "^10.5.0",
"@trpc/next": "^10.5.0",
"@trpc/react-query": "^10.5.0",
"@trpc/server": "^10.5.0",
"@types/styled-components-react-native": "^5.1.3",
"next": "12.1.6",
"nextjs-cors": "^2.1.2",
"react": "18.1.0",
"react-dom": "18.1.0",
"react-is": "^18.2.0",
"styled-components": "^5.3.5",
"superjson": "^1.12.0",
"swagger-ui-react": "^4.15.5",
"trpc-openapi": "^1.0.0",
"zod": "^3.20.2"
},
"devDependencies": {
"@types/node": "17.0.36",
"@types/react": "18.0.9",
"@types/react-dom": "18.0.5",
"@types/styled-components": "^5.1.25",
"@types/swagger-ui-react": "^4.11.0",
"eslint": "8.16.0",
"eslint-config-next": "12.1.6",
"typescript": "4.7.2"
},
"resolutions": {
"styled-components": "^5"
}
}
I would like to migrate a legacy REST API with an OpenAPI spec to TRPC. In order to do so we must accept application/x-www-form-urlencoded
request bodies.
Just wondering if there is a quick example on how to add the oauth 2.0 authorization protocol for endpoint protection?
export const campaign = router({
findOne: publicProcedure
.meta({
openapi: {
method: 'GET',
path: '/campaign/findOne',
description: 'Will return a individual admanager campaign by id'
}
})
.input(z.object({ id: z.string() }))
.output(Campaign)
.query(({ input }) => {
console.log('input', input);
return {
id: '0a1ee26ae7c739472a9ca53810a661d1',
name: `campaign name: ${input.id}`
};
}),
});
The above works fine with the client by using: trpc.campaign.findOne.useQuery({ id: 'xyz12374574' });
However, I've tried changing the path to /campaign.findOne
& /campaign.findOne/{id}
& /campaign.findOne?id={id}
but no success, seems to not format the url correctly to resolve the response, any idea why?
[\n {\n \"code\": \"invalid_type\",\n \"expected\": \"object\",\n \"received\": \"undefined\",\n \"path\": [],\n \"message\": \"Required\"\n }\n]
This is the error I'm getting back in swagger
I've been using errorFormatter to include Zod errors and more custom errors, but the returned
error type in openapi schema doesn't synchronize
trpc error formatter:
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
errCode: error.cause instanceof CustomTRPCError ? error.cause.errCode : null,
zodError:
error.code === 'BAD_REQUEST' &&
error.cause instanceof ZodError
? error.cause.flatten()
: null,
},
};
}
return open-api schema:
{
"message": "string",
"code": "string",
"issues": [
{
"message": "string"
}
]
}
real response:
{
"message": "Input validation failed",
"code": "BAD_REQUEST",
"issues": [
{
"validation": "uuid",
"code": "invalid_string",
"message": "Invalid uuid",
"path": [
"keyRingKeyId"
]
}
]
}
Initially proposed by @mshd (#55)
Additionally raised by @theobr (https://youtu.be/YAzzvhaRs6M?t=7086)
meta
complexityIt has been mentioned a few times that the meta
required to enable OpenAPI support on a tRPC procedure could be reduced. This RFC proposes setting some sensible defaults (can be overwritten), such that the minimum required meta
will be as follows:
{ openapi: { enabled: true } }
meta.openapi.enabled
: true
meta.openapi.method
: GET
| POST
| PUT
| PATCH
| DELETE
meta.openapi.path
: string
meta.openapi.enabled
: defaults to false
meta.openapi.method
: defaults to GET
(query) | POST
(mutation)meta.openapi.path
: defaults to computed value from tRPC procedure where:
/
path.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase())
const postsRouter = t.router({
getById: t.procedure
.meta({ openapi: { enabled: true } }), // { openapi: { enabled: true, method: 'GET', path: '/posts/get-by-id' } }
.input(z.object({ id: z.string() })),
.output(postSchema)
.query(({ input }) => {
...
})
...
})
const appRouter = t.router({
posts: postsRouter,
...
})
RESTful
API design patterns in most cases, examples:
/posts/get-by-id
should be /posts/{id}
.PUT
or PATCH
.path
will change.Not fully sold on this change, any thoughts are welcomed (cc @KATT @sachinraja)
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.