Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pure RPC client types #2489

Open
askorupskyy opened this issue Apr 9, 2024 · 17 comments
Open

Pure RPC client types #2489

askorupskyy opened this issue Apr 9, 2024 · 17 comments
Labels
enhancement New feature or request.

Comments

@askorupskyy
Copy link

askorupskyy commented Apr 9, 2024

What is the feature you are proposing?

The problem

I was using Hono for one of my monorepo projects and one of the problems I have is that it fails to compile whenever the process environments are different.

Some to code to better explain:

API environment

This below contains all sorts of stuff the API might need, like DB connection strings, payment credentials...

declare global {
  namespace NodeJS {
    interface ProcessEnv {
      API_URL: string;
      APP_URL: string;
      REDIS_URL: string;
      MYSQL_URL: string;
      // etc....
    }
  }
}

Nextjs web client environment

declare global {
  namespace NodeJS {
    interface ProcessEnv {
      API_URL: string;
      APP_URL: string;
    }
  }
}

Compile issues:

How since the Hono RPC client is just the representation of the API, it should not care for missing environment type definitions on the backend.

However, when compiled, (I use Nx with esbuild), the frontend application complains about missing backend environment types, even though this stuff should be abstracted.

I should not need the REDIS_URL to be in process.env within my Nextjs app, however, Hono makes me do this...

Possible fix:

Now I've done some digging and it does not make sense to build ALL of the routes, including their actual implementations in order to have a client type.

  • This slows down the tsc compilation times
  • Makes it harder to separate client and non-client environment, therefore harder to maintain.

It would make much more sense to have a route type, aka input->output definition, from which the client would be constructed. The actual routers would then import this type and have the actual logic.

This, in pair with zod-openapi package would make Hono a killer RPC platform.

How do Hono's competitors achieve this?

I've also been using https://ts-rest.com/, which follows pretty much the same pattern I'm proposing here. The only difference is ts-rest follows full OpenAPI specs for their API definitions.

For example,

This code below describes the type of the router (the client inherits directly from here...)

export const contract = c.router({
  createPost: {
    method: 'POST',
    path: '/posts',
    responses: {
      201: PostSchema,
    },
    body: CreatePostSchema,
    summary: 'Create a post',
  },
})
@askorupskyy askorupskyy added the enhancement New feature or request. label Apr 9, 2024
@askorupskyy
Copy link
Author

How a found a little way to make this work:

Below is a pure type that does not care about the contents of route implementation.

For now I am just going to create a separate library with such types for each router...

export type AppType = Hono<
  Env,
  ToSchema<
    'get',
    '/hello',
    {
      query: {
        test: string;
      };
    },
    Promise<
      TypedResponse<{
        hello: string;
      }>
    >
  >,
  '/'
>;

const client = hc<AppType>('localhost');

client.hello.$get({ query: { test: 'hello' } });

@askorupskyy
Copy link
Author

askorupskyy commented Apr 9, 2024

I tried the same thing but with @hono/zod-openapi, and here's what I got

const route = createRoute({
  method: 'get',
  path: '/hello',
  request: {
    params: z.object({ test: z.string() }),
  },
  responses: {
    200: {
      content: {
        'application/json': {
          schema: z.object({ hello: z.literal('world') }),
        },
      },
      description: 'Get hello!',
    },
  },
});

const api = new OpenAPIHono();
const APP = api.openapi(route, function () {} as any);

const client = hc<typeof APP>('localhost');
const req = await client.hello.$get({ param: { test: '123123' } });


const res = await req.json();
// === "world"
console.log(res.hello)

This gave me full control over the types I have in my client, so I managed to create a whole client without any actual code.

This same route variable could now be used in a separate lib/file to actually implement this endpoint.

@yusukebe
Copy link
Member

Hi @askorupskyy

Is the matter resolved? If not, please provide a minimal project to reproduce it. I'll investigate.

@askorupskyy
Copy link
Author

I'll make a project and tag you in a couple hrs.

I found a hack around original Hono type behavior but this would be nice to have as a part of the framework.
Essentially the problem is in order to create the AppType that you'd use in hc, Hono needs to compile every single type that's used in the router (even when it does not affect the input/output), even though it's not really needed for the client to work properly. What I propose is some way to create this AppType without actually writing the router logic, which I was able to achieve in the examples above.

This little hack that I put above worked for me and actually reduced the amount of .ts stuff needed to be compiled for the frontend. I use prisma for my project so that was significant.

@askorupskyy
Copy link
Author

Hi @askorupskyy

Is the matter resolved? If not, please provide a minimal project to reproduce it. I'll investigate.

https://github.com/askorupskyy/hono-rpc-types

I created a simple project that explains the issue. Follow the README and the code

@yusukebe
Copy link
Member

@askorupskyy

Thanks!

@askorupskyy
Copy link
Author

I read through some more issues and I think using my proposal would also fix #1921. Seems like one of the cases I covered in the README to my repo

@yusukebe
Copy link
Member

@askorupskyy

Hmm. I'm trying it, but I'm not so familiar with Nx, and I can't understand what causes the issue. I think it's fine to simply make it emit d.ts and import the file into the client.

@askorupskyy
Copy link
Author

@yusukebe Nx in my case is just used to demonstate some of the bugs the current type system might cause. The main problem is Hono client needs to infer all the types from routes directly, which causes all of the code inside .get(), .post() and etc. to be compiled by tsc.

At my company we've been using tRPC which had a similar issue. It was building too many types and causing our builds to fail due to lack of RAM. We later switched to ts-rest, which creates the types in advance, similar to what #1921 is proposing,

RPC client's type should be known beforehand so that I can declare it in a global app.d.ts file and pass it int the request.locals to get access in the entire app.

Essentially what I did in this repo is a created the AppType myself using @hono/zod-openapi lib and it allowed me to create a client without compiling the API implementation myself.

It would be nice to declare your route types in Hono the same way as in ts-rest.

@askorupskyy
Copy link
Author

askorupskyy commented Apr 13, 2024

This syntax would work better:

// declare a router
const createUserRouter = createRouter({
  method: 'POST',
  input: // either a TS type or a zod object
  output: // just a TS type
})
// implement a router
// use generics to make sure what you are implementing matches the type of the declaration
const router = new Hono().post<typeof createUserRouter>('/user', (c) => c.json(new User()))
// create a client
const client = hc<typeof createUserRouter>();

This way the client only depends on the type of the declaration, not the entire business logic of the app.
Also this will speed up the Intellisense for the client, since on every change it's not going to have to recompile the entire business logic

@yusukebe
Copy link
Member

@askorupskyy Thanks for explanation.

In the future, we may add a Validator that validates Response (#2439), so if we can infer the type from that Validator, we will not need to generate the type from the entire Hono application.

@yusukebe
Copy link
Member

Either way, this is not a problem that can be solved right away. I'd like to take some time to think about it.

@yusukebe
Copy link
Member

Plus. As you @askorupskyy mentioned above, the one way to solve this issue is using Zod OpenAPI. Because it is designed to declare endpoint types explicitly. So, we may not have to make Hono core's type inferences more complicated to support this matter. Just use Zod OpenAPI.

@askorupskyy
Copy link
Author

askorupskyy commented Apr 13, 2024

@yusukebe thank you so much! i did not initially realize zod-openapi resolves this matter completely

@fzn0x
Copy link
Contributor

fzn0x commented May 3, 2024

Hi @yusukebe and everyone, I think this issue solved in #2499 in case you need help to track the progress of this issue, correct me if I'm wrong. ;)

@yusukebe
Copy link
Member

yusukebe commented May 3, 2024

Hi @fzn0x

#2499 does not resolve this. This issue concerns whether or not we can take types from each route. It is not related to it. Either way, Zod Open API has solved this, so we may close this.

@bruceharrison1984
Copy link
Contributor

bruceharrison1984 commented May 3, 2024

@yusukebe, I believe this change may have broken ZodOpenAPI type inferrence:
honojs/middleware#496

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request.
Projects
None yet
Development

No branches or pull requests

4 participants