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

Enhanced TypeBox support for Fastify #3391

Closed
2 tasks done
sinclairzx81 opened this issue Oct 23, 2021 · 11 comments
Closed
2 tasks done

Enhanced TypeBox support for Fastify #3391

sinclairzx81 opened this issue Oct 23, 2021 · 11 comments
Labels
documentation Improvements or additions to documentation good first issue Good for newcomers

Comments

@sinclairzx81
Copy link
Contributor

sinclairzx81 commented Oct 23, 2021

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure the issue has not already been raised

Issue

Hi. I'm writing to submit a possible enhancement for Fastify + TypeBox users.

Given that the TypeBox library sees a fair amount of use in Fastify projects, I thought I'd see if there were ways to enhance the TypeBox user experience specifically for Fastify. The work primarily focused on removing the need to explicitly pass generic type parameters on Fastify routes, as well as removing the need for users to call TypeBox's Static<T> type. I felt the ideal solution was to try and automatically derive Fastify request and reply types directly from the TypeBox schemas, so the work primarily was centered on this aspect.

I've setup an initial project which can be located here that provides such enhancements. The project is very much in a prototype proof of concept state and looking for community feedback. Also, I feel there may also be some potential here provide auto inference for direct JSON schema > TypeScript type mappers (without the use of TypeBox), so this project may serve as a possible integration path for that.

Submitting for community feedback.

Current Usage

Current usage requires keeping schema types external to the route so they can be passed to Static<T>. Additionally the Fastify route requires explicit generic arguments to ensure static checking inside the route.

import { Type, Static } from '@sinclair/typebox'

import Fastify from 'fastify'

const fastify = Fastify()

const Querystring = Type.Object({
    a: Type.Number(),
    b: Type.Number()
})

const Response = Type.Number()

fastify.post<{ 
    Querystring: Static<typeof Querystring>, 
    Reply: Static<typeof Response>
}>('/add', { 
    schema: {
        querystring: Querystring,
        response: {
             200: Response
        }
    }
}, (req, reply) => 
    reply.status(200).send(req.query.a + req.query.b)
)

Enhanced Usage

Request and Reply types automatically inferred and statically checked in the route handler. You can test the type inference here.

import { FastifyTypeBoxInstance, Type } from 'fastify-typebox'
import Fastify from 'fastify'

const fastify = Fastify() as FastifyTypeBoxInstance

fastify.get('/add', { 
    schema: {
        querystring: Type.Object({
          a: Type.Number(),
          b: Type.Number()
      }),
      response: {
         200: Type.Number()
      }
    }
}, (req, reply) => 
    reply.status(200).send(req.query.a + req.query.b)
)
@mcollina
Copy link
Member

Will these changes to Fastify require adding typebox as a direct dependency? What other downside would have?

cc @fastify/typescript

@sinclairzx81
Copy link
Contributor Author

sinclairzx81 commented Oct 23, 2021

@mcollina Hi. There's actually no changes required in Fastify to enable this. The library works over the top of Fastify (3.22.1) by remapping its type definitions using the TypeScript type system only.

Users can opt into auto inference by installing fastify-typebox and adding a FastifyTypeBoxInstance type assertion immediately following a call to initialize Fastify. This will remap Fastify's route handlers (GET, POST, PUT, etc), but leaves the rest of the Fastify interface intact.

import { FastifyTypeBoxInstance } from 'fastify-typebox'

const fastify = Fastify() as FastifyTypeBoxInstance // enable auto inference

In terms of downsides, there are no functional changes (all this is happening entirely within the TypeScript type system). In terms of type differences, Fastify TypeBox rebuilds the FastifyRequest and FastifyReply types to be supportive of a new FastifyTypeBoxSchema interface (shown below, which helps facilitate the auto inference), but everything else should be exactly the same type wise.

export type FastifyTypeBoxSchema = {
    body?:        TSchema,
    headers?:     TSchema,
    querystring?: TSchema,
    response?:    { [statusCode: number]: TSchema }
}

At this stage, I'm treating the library as a proof of concept. I did want to share this approach as it does completely remove the need for Static<T> as well as the explicit generic type hinting on routes; making things extremely easy for end users.

Additionally, the technique used to augment Fastify types may prove useful to other JSON schema mappers, such as json-schema-to-ts, so I felt it was worth sharing on those grounds too.

Lastly, I do actually get a few Fastify usage questions on the TypeBox project, so did want to provide something to help streamline usage scenarios specifically for Fastify. Given the nature of the library, I would actually be open to donating the Fastify TypeBox library to the Fastify organization for continued development if it ends up being helpful for users of either project.

Cheers!
S

@mcollina
Copy link
Member

I think we should document this in our docs, it looks amazing!

@climba03003 climba03003 added documentation Improvements or additions to documentation good first issue Good for newcomers labels Oct 23, 2021
@rickerp
Copy link

rickerp commented Oct 25, 2021

Is this already implemented? Because it's causing me some errors
image
I also tried to use the default import Fastify() from typebox but the result is similar. Of what I understand its because I need to replace what I use of the original fastify with the fastify-typebox types and imports. This has two problems, one, there is no FastifyTypeBoxPluginAsync, second for other third party plugins like fastify-swagger I can't change the type in there.

@sinclairzx81
Copy link
Contributor Author

sinclairzx81 commented Oct 25, 2021

@rickerp It should now. If you grab fastify-typebox@0.8.13, I've added an additional overload to allow you to use existing plugins. Note that if the plugin exposes fastify request or reply types (for example uiHooks), these types be will the original fastify request and reply types, not the type inferable ones.

Example here. (note you may need to clear your browser cache to pull the latest dependency)

import Fastify, { Type } from 'fastify-typebox'
import fastifySwagger    from 'fastify-swagger'

const fastify = Fastify()

fastify.register(fastifySwagger, {
  routePrefix: '/documentation',
  swagger: { /* */ },
})

fastify.post('/hello/:world', {}, (request, reply) => {
  reply.send(request.params.world)
})

I wonder if it might be a good idea to move additional issues over to the fastify-typebox repository. Would be good to gather all these edge cases to ensure things work as expected. Keep in mind this project is very much in the work in progress state, so expect a few issues here and there.

Let me know how you go.
S

@sinclairzx81
Copy link
Contributor Author

sinclairzx81 commented Oct 25, 2021

I also tried to use the default import Fastify() from typebox but the result is similar. Of what I understand its because I need to replace what I use of the original fastify with the fastify-typebox types and imports. This has two problems, one, there is no FastifyTypeBoxPluginAsync, second for other third party plugins like fastify-swagger I can't change the type in there.

@rickerp I'm still working on how best to expose these type mappings so things are a little in flux here. Currently there are two ways to opt in.

import Fastify, { Type } from 'fastify-typebox'

const fastify = Fastify() // All setup with typebox
import { Type, FastifyTypeBoxInstance } from 'fastify-typebox'

import Fastify from 'fastify'

const fastify = Fastify() as FastifyTypeBoxInstance // augment an existing instance

While the first option is more ergonomic, the second is a bit more in-tune with how I'd prefer fastify-typebox to work.

@Ethan-Arrowood
Copy link
Member

Definitely +1 on adding this to the documentation. The discussion itself is still highly active over a year later so i think it would be valuable to have more info on typescript/routes/typebox in the documentation site

@fox1t
Copy link
Member

fox1t commented Oct 25, 2021

I like the as FastifyTypeBoxInstance implementation because then we can support different libraries. However, I wonder if we can include a concept of provider inside fastify types itself.

@climba03003
Copy link
Member

climba03003 commented Oct 25, 2021

I think it is the right time to re-visit the typing system for fastify and update it for fastify@4.

Maybe we can merge the argument of FastifyInstance and allow it to override optionally by third-party.

export interface FastifyInstance<
RawServer extends RawServerBase = RawServerDefault,
RawRequest extends RawRequestDefaultExpression<RawServer> = RawRequestDefaultExpression<RawServer>,
RawReply extends RawReplyDefaultExpression<RawServer> = RawReplyDefaultExpression<RawServer>,
Logger = FastifyLoggerInstance
> {

@fox1t
Copy link
Member

fox1t commented Oct 25, 2021

@climba03003 I agree with you that v4 is the perfect timing! Our types are very powerful but verbose too. Maybe providing better defaults or this concept of provider could make them even better!

@sinclairzx81
Copy link
Contributor Author

@climba03003 If it's helpful, I have actually been experimenting on a fastify fork exploring the Type Provider idea. I've actually managed to get Request schematics piping through and inferencing. Current implementation is quite rough, but the changes required to enable it can be viewed here https://github.com/sinclairzx81/fastify/commit/f2146ae46f01178faeeadc042a4775831a2f2245

Updates mostly included:

  • Propagate the SchemaCompiler (or FastifySchema) into dependent types (Request, Reply). This was used to extract the schema from the user supplied Options.
  • Update the Request, using the SchemaCompiler type to remap the schema properties for the request
  • Implement Type Provider (essentially just a HKT) that allows for injecting the providers.
  • Propagate Type Provider through to Request / Reply and infer at Request site.

Current interface is roughly as follows.

import { Fastify, FastifyTypeProvider } from 'fastify'
import { Static } from '@sinclair/typebox'
import { FromSchema } from 'json-schema-to-ts'

// typebox
interface TypeBoxTypeProvider extends FastifyTypeProvider {
    output: Static<this["input"]>
}

// json-schema-to-ts
interface JsonSchemaTypeProvider extends FastifyTypeProvider {
    output: FromSchema<this["input"]>
}

const fastify = Fastify().typeProvider<TypeBoxTypeProvider>()

And a working screenshot.

image

Hope this helps
S

@sinclairzx81 sinclairzx81 changed the title Enhanced TypeBox support for Fastify (Auto type inference) Enhanced TypeBox support for Fastify Oct 29, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation good first issue Good for newcomers
Projects
None yet
Development

No branches or pull requests

7 participants