hayes / pothos

Pothos GraphQL is library for creating GraphQL schemas in typescript using a strongly typed code first approach
https://pothos-graphql.dev
ISC License
2.31k stars 158 forks source link

[Feature request] Late resolver declaration #548

Open LMaxence opened 2 years ago

LMaxence commented 2 years ago

Hello !

First of all, thanks a lot for Pothos, I have been playing around with it lately, and it is very comfortable and nice to use. I always try to push type safety as far as possible, so I really like the approach you had on this schema builder :)

Feature request

I think, though, that it would be nice to be able to declare the resolvers implementation separately from the schema types. Something like:

const builder = new SchemaBuilder({});

builder.queryType({
  fields: (t) => ({
    hello: t.string({
      args: {
        name: t.arg.string(),
      },
      // No `resolve` attribute
    }),
  }),
});

// This would work fine, any resolver called at this point would throw something like `new Error("Not implemented")`

export const schema = builder.toSchema({})

// Then later or anywhere else in the codebase

builder.defineResolver({
  query: {
    hello: () => "Hello, world !" // Ideally, this would remain type-safe
  }
})

Motivation

In large projects, the resolvers usually include a lot of domain-related code, dependencies, etc.

It eventually leads to slow builds, and heavy codebases. Even worse, if I use the schema output from Pothos and generate a client out of it as it is described here, not only my backend is slow to transpile, but so becomes my frontend.

Smaller projects can afford to rely on the pattern "the API declares a graphql schema, and thus a client is generated to be used afterwards in the frontend". As long as the API remains lightweight, fast to transpile, generally-speaking simple, well this chain-like pipeline is not to be worried about, because everything is fast.

That being said, larger projects will quickly suffer from such a pipeline. If my API takes, let us say 30 seconds to be transpiled on my local computer, then every transpilation of my frontend code will start with a 30 seconds long API transpilation, before even it can generate the client to use.

In CIs, it is dramatically worse: In addition to a slower build (30 seconds locally can easily lead to minutes in a CI/CD pipeline), I need to download and install a much larger amount of dependencies before I can even start the build pipeline of the frontend. All of these dependencies, except the graphql package are not necessary for pothos to declare my schema, and thus generate a client, because they are all related to business logic declared in the resolver.

Larger organizations see GraphQL as a leverage to turn the dependency chain into something like "The frontend team and the backend team settle on an interface to follow, and then they are loosely-coupled for the implementation details". This is precisely what I would like to reach in my team. This ability would lead to huge improvements in our CI time as well.

Current solutions

Currently, it is possible to hack into a result that looks a bit like this using the mocks plugin. Yet, it feels like a square peg in a round hole (not sure about this expression but that is the best translation I got for the french "Mettre des ronds dans des carrés").

If you feel like it could be a valuable addition to Pothos, I am 100% willing to discuss this further with you and to provide contributions for having it implemented. If anything in here is not clear, feel free to ask for clarifications, I would be happy to provide them.

Once again, Pothos is really cool, and I would love to make it fit my requirements in order to use it in my team.

hayes commented 2 years ago

The biggest issue I see with building something like this is that you have no way to type the resolvers. There is no good way to propagate the type information from where you define the fields to where you add the resolvers.

Out of curiosity, is this 30 seconds a real scenario you are running into, or more hypothetical? I've worked with a couple big schemas, and was still able to geberate the schema in a couple of seconds. There might be an easy way to optimize something slow in your process that doesn't require rearchitecting everything.

That doc only describes one of many ways to generate client types. I almost always output my schema to a schema.graphql file, which is checked into the repo. This allows you to build the UI independently

I have also seen some work on a esbuild plugin that automatically strips out resolvers and related imports so that the schema can be built with much less code.

hayes commented 2 years ago

I'm not opposed to exploring something like this, I just don't have a good idea about how you could type the resolvers with that API.

hayes commented 2 years ago

Example of a script to write your schema to a file: https://github.com/hayes/pothos/blob/main/examples/complex-app/scripts/build-schema.ts. It's is also common to just print the schema anytime it's built (maybe behind a flag in dev). In this case it's run as a package.json script: https://github.com/hayes/pothos/blob/fda3f27217dd8f69a5f19b553a9c8a67bf0b9c40/examples/complex-app/package.json#L9. Using swc works pretty well for speed

LMaxence commented 2 years ago

Thanks for your answer :)

Out of curiosity, is this 30 seconds a real scenario you are running into, or more hypothetical?

It is, but not with Pothos. Our current API is based on NestJs, and our build problem is more of a 1 minute problem. I am currently exploring a switch to other technologies for that very reason, and definitely your lib is my final candidate. If I have good hope to solve my 30-seconds-problem by just making the switch to Pothos (our API is not that much big as we speak), I fear it will inevitably come back as long as the API grows, for the reasons I mentioned above (essentially resolvers becoming larger).

I like the idea of keeping an output of the schema somewhere. Technically, in the code-first approach the schema is a build artifact, so it feels unnatural to commit it. That being said, it remains the most pragmatic solution to my problem so far.

I'm not opposed to exploring something like this, I just don't have a good idea about how you could type the resolvers with that API.

If you are okay with it, I will keep digging this in the following weeks. I truly think that solving this technical problem (if ever it is possible) would allow a lot of projects to move out from the hacky solutions we described so far.

hayes commented 2 years ago

I like the idea of keeping an output of the schema somewhere. Technically, in the code-first approach the schema is a build artifact, so it feels unnatural to commit it. That being said, it remains the most pragmatic solution to my problem so far.

It's a little weird, but it is extremely useful in code reviews.

If you are okay with it, I will keep digging this in the following weeks. I truly think that solving this technical problem (if ever it is possible) would allow a lot of projects to move out from the hacky solutions we described so far.

For sure, let me know if you come up with anything.

I fear it will inevitably come back as long as the API grows, for the reasons I mentioned above (essentially resolvers becoming larger).

tools like esbuild and swc are very fast, the time to build you schema should be inline with the amount of time it takes to start your app in production. If it is taking a long time just to start your app, that might be a bigger issue.

lo1tuma commented 5 months ago

I would be interested in something like this as well. Currently there seems to be no easy way to support dependency-injection for resolvers. It might be possible with Context but it feels not right.