honojs / hono

Web framework built on Web Standards
https://hono.dev
MIT License
18.32k stars 515 forks source link

[Proposal] RPC #727

Closed yusukebe closed 1 year ago

yusukebe commented 1 year ago

I'll introduce an ultra cool feature. It will improve our developer experience. This issue is a continuation of #602.

"tRPC" is helpful for sharing the types between the server and the client. This means it makes the types an API specification. There is a request to make tRPC adapter #582 , I think it's good to make the adapter as 3rd party middleware.

So, tPRC is great, but we can do it in a better way with Hono because we can make it "integrated".

What we want to do are below:

Then, I have created those prototypes. They are not fully implemented, but they work. See the screencast:

output

Server-side code:

const api = new Hono()

const getRoute = api
  .get('/hello', (c) => {
    return c.jsonT({
      messages: 'hello',
    })
  })
  .build()

const postRoute = api
  .post(
    '/posts',
    validator((v) => ({
      id: v.json('id'),
      title: v.json('title'),
    })),
    (c) => {
      const post = c.req.valid()
      return c.jsonT({ title: post.title })
    }
  )
  .build()

export type AppType = typeof postRoute & typeof getRoute

Client-side code:

import type { AppType } from './server'

const client = new Client<AppType>('http://127.0.0.1:8787/api')

const res = await client.get('/hello')

console.log(res.messages)

const res2 = await client.json('/posts', {
  id: '123',
  title: 'Hello!'
})

console.log(res2.title)

The client-side needs to be considered, but this is a good idea for the server-side API. And, if developers want to use 3rd party validator like a "Zod", they may use it.

Notes, we need to brush up on our data structures to handle types with Generics.

Isn't it cool? I may create the PR later.

cleaton commented 1 year ago

Looks great! I like the API.

Some thoughts I’ve had wile working with my “type-knit” project that might be relevant here as well:

  1. Cloudflare Durable Objects & services are exposed as stubs with a “fetch” method. This stub could have a client wrapper (that does not need url) to be used on workers (micro service RPC)
  2. It might be best if the client returns a composable type instead of directly returning the api type. Ex ‘Result< T >’ . This way response object could be passed from DO to worker to web client without serializing in worker (only deserialize on result unwrap). Ex: ‘’’worker

const postRoute = api .post( '/posts', validator((v) => ({ id: v.json('id'), title: v.json('title'), })), (c) => { const post = c.req.valid() return DoClient(dostub).json(“/increment”, {title: post.title}) } ) .build() ‘’’

Talent30 commented 1 year ago

Looks great! Is the client a wrapper of the web native fetch?

yusukebe commented 1 year ago

@Talent30

Yes, I'm planning to wrap fetch though the client has not been designed yet.

Talent30 commented 1 year ago

@yusukebe Thank you for your response! Have you thought about how it would handle multipart/form-data?

cleaton commented 1 year ago

@yusukebe Thank you for your response! Have you thought about how it would handle multipart/form-data?

You are thinking about file upload? I think each client method is a specific spec. For example client.json() is a POST request with content-type application/json. Similarly there could be a client.upload() that uses multi-part though I’m not sure that’s need for a first release to keep things simple.

yusukebe commented 1 year ago

@Talent30 @cleaton

Of course I am thinking about that. I think we can implement c.form() or the more generic c.post().

BTW,

At the moment, I am working on an API design for "client". I am looking into other libraries and trying to create the "client" prototypes.

This has almost nothing to do with Hono, but I also created this interesting client.

https://github.com/yusukebe/xc

I think it is a pretty good idea to be able to specify such headers and queries in the callback.

const json = await client
  .get('/shops/:shopId', (c) => {
    c.header('User-Agent', 'Xc')
    c.param('shopId', 'yoshimuraya')
  }).json()

It's just an idea. I need to consider it more.

yusukebe commented 1 year ago

Also, and this is still undecided too, I am thinking of deprecating the built-in Validator and doing Validation with such "Zod". I really like the Built-in Validator, but implementing the Validator on my own is a difficult thing to do. It also complicates the code.

For example, I'm trying the following implementations.

const postRoute = app
  .post(
    '/post',
    zValidator(
      'json',
      z.object({
        title: z.string(),
        body: z.string(),
      })
    ),
    async (c) => {
      const { title, body } = c.req.valid()
      c.jsonT({ message: `${title} : ${body}` })
    }
  )
  .build()

const authorRoute = app
  .post(
    '/author',
    zValidator(
      'form',
      z.object({
        name: z.string(),
        age: z.number(),
      })
    ),
    async (c) => {
      const { name, age } = c.req.valid()
      c.jsonT({ message: `${name} : ${age}` })
    }
  )
  .build()

type AppType = typeof postRoute & typeof authorRoute
SS

Pretty good, right?

This will be a separate Issue.

There are still many decisions that need to be made. However, I would like to take my time.

cleaton commented 1 year ago
const json = await client
  .get('/shops/:shopId', (c) => {
    c.header('User-Agent', 'Xc')
    c.param('shopId', 'yoshimuraya')
  }).json()

It's just an idea. I need to consider it more.

I am working on similar experiments. Instead of callback with context thought to make the client api argument always be of type {headers: Record<string,string>, params: Record<string,string>, body: T} .

yusukebe commented 1 year ago

Yes, this approach is also good.

{headers: Record<string,string>, params: Record<string,string>, body: T} 
yusukebe commented 1 year ago

This is the current status:

https://user-images.githubusercontent.com/10682/209410398-0bc8f62f-67ad-4699-8d49-8f66a2ad77ee.mov

These are just ideas!

yusukebe commented 1 year ago

If we create zod-validator, I am wondering if we should make it a built-in middleware or @hono/zod-validator as a 3rd-party. This is because the policy is that built-in middleware should not depend on external libraries.

But this Validator is important. It might be better to make it a built-in middleware. It is also easier to maintain. So, how about do not include it in dependencies, but install zod when the developer want to use it. If so, we have to specify it in the peerDependencies section.

cleaton commented 1 year ago

@yusukebe the demo looks amazing, good job. Personally i think the zod validator should be separate. Better keep the core without specific 3rd party validators and instead only expose a validator Interface ex (unknown) => T. For example if you use this API to create RPC access to internal durable objects it might be enough to only have compile time type errors for clients and no actual runtime validations (worker - DO connection is trusted). Zod can be too heavy for some cases.

yusukebe commented 1 year ago

Created the PR related to this issue: https://github.com/honojs/hono/pull/745

b3nten commented 1 year ago

This looks great, but I think the client API could be slightly improved. Like with Eden the routes could be called using a chained method call. So /api/users/getUser could be called via client.api.users.getUser(). I think this API is more robust than using a string.

Also I agree with @cleaton about keeping the zod validation seperate.

yusukebe commented 1 year ago

This is the current status:

https://user-images.githubusercontent.com/10682/210257864-58f05fdb-fb67-4d10-ade0-5686091f62dd.mov

Stack:

transitive-bullshit commented 1 year ago

Love this work. Would vote heavily in favor of making zod the default validator. It just has so much adoption and integrations throughout the TS community. Honestly, the number one thing that turned me off when first going through the hono docs was that it used yet another custom validation library. I understand why, and I agree that you can make things generic and have a zod-validator; just keep in mind that every addition of a generic option increases complexity for the end developer to understand and choose between.

Really excited about this feature as a much more lightweight alternative to trpc, which in my experience just feels really bloated in terms of the number of different moving parts that it takes to get a basic full-stack, type-safe API up & running.

yusukebe commented 1 year ago

@transitive-bullshit

just keep in mind that every addition of a generic option increases complexity for the end developer to understand and choose between.

Yes. I could feel the importance of this in the development of Hono over the past year. I would like to design the API with this in mind. Thanks.

izakfilmalter commented 1 year ago

The thing that I value about trpc is it's easy integration into react/nextjs via tanstack/query. Is there a future where this could be wrapped like that to make the transition from trpc to hono easier?

yusukebe commented 1 year ago

Current status:

https://user-images.githubusercontent.com/10682/215533246-8dcd3e58-c834-4629-b826-03fc43fe4820.mov

I'm re-designing the client API.

a-eid commented 1 year ago

@yusukebe

would it be possible to batch up requests that happen at the same time into one like TRPC ? how to test your current progress ? how would I be able to help ?

thanks.

yusukebe commented 1 year ago

hc or "Hono Client" is just a very thin wrapper of fetch. I think we can implement batch with some ideas.

kevbook commented 1 year ago

This client can perhaps be used in writing tests. Can you show an example of that, please?

yusukebe commented 1 year ago

Hi there.

I've created the PR for introducing "Hono Client". This was created as @hono/hc but made obsolete and include in the core package because of DX.

https://github.com/honojs/hono/pull/862

It may be difficult to try since we haven't published it as npm yet, but you can see it in the PR comment and the tests.

yusukebe commented 1 year ago

v3 including PRC-mode has been released!

https://github.com/honojs/hono/releases/tag/v3.0.0

Try it!

a-eid commented 1 year ago

@yusukebe I have a couple of questions ?

yusukebe commented 1 year ago

@a-eid

Ref: https://hono.dev/guides/rpc

does it work with bun ?

Yes.

yusukebe commented 1 year ago

Done!

robertveloso commented 1 year ago

hc or "Hono Client" is just a very thin wrapper of fetch. I think we can implement batch with some ideas.

Sorry to open an old thread, but I could find any information about the batch in Hono client, is it possible?

yusukebe commented 1 year ago

Hi @robertveloso !

batch is not implemented yet. Thanks.