Coder Social home page Coder Social logo

coobaha / typed-fastify Goto Github PK

View Code? Open in Web Editor NEW
65.0 2.0 3.0 1.87 MB

Better typescript support and runtime safety of fastify handlers

Home Page: https://tfs.cooba.me

JavaScript 47.29% TypeScript 52.17% Shell 0.04% Earthly 0.50%
fastify json-schema typescript validation types schema-generation schema

typed-fastify's Introduction

Typed Fastify

Build Status Coverage Status NPM Version

Online docs

This package adds strong TypeScript support to Fastify request handlers and enforces handlers to have typed schema which is used to validate request params and replies. From this schema it does two things:

  • static typechecking against TypeScript Schema
    • request.body
    • request.headers
    • request.querystring
    • request.params
    • route.path.params are also inferred and mapped to request.params, it is also not possible to make a typo in schema params
    • reply is always based on status, developer won't be able to use plain reply.send() but forced to explicitly set status first, based on which response type will be inferred
  • JSON schema generation from TS Schema (using typescript-json-schema with custom transforms, all @tjs annotations can be used to fine-tune output)
    • since we use typejescript-json-schema: all known limitations of lib are inherited:
      • Records are not transformed correctly, use { [k: string]: string } instead or hint with @tjs
  • Runtime validation using generated JSON schema (optional but strongly recommended as it brings extra safety to runtime and ensures that code assumptions about data are correct)
2021-02-18_19-50-09.mp4

Usage

npm i @coobaha/typed-fastify

pnpm i @coobaha/typed-fastify

yarn add @coobaha/typed-fastify

Example of service we want to build

GET / => Hello ($querystring.name || world)

Simple implementation without schema generation will be following

import addSchema, { Schema } from '@coobaha/typed-fastify';
import fastify from 'fastify';

export interface ExampleSchema extends Schema {
  paths: {
    'GET /': {
      request: {
        querystring: {
          name?: string;
        };
      };
      response: {
        200: {
          content: string;
        };
      };
    };
  };
}

const exampleService: Service<ExampleSchema> = {
  'GET /': (req, reply) => {
    // typescript will infer correct types for us
    const name = req.query.name ?? 'World';

    // Calling send directly is not allowed
    // reply.send(`Hello ${name}`)
    // Calling send with wrong payload will result in an error
    // reply.status(200).send(new Date())

    return reply.status(200).send(`Hello ${name}`);
  },
};

const app = fastify();
addSchema(app, {
  // it is strongly recommended to generate json schema to guaruntee runtime validity
  jsonSchema: {},
  service: exampleService,
});

// Start listening.
app.listen(3000, (err: any) => {
  if (err) {
    app.log.error(err);
    process.exit(1);
  }
});

Complex examples can be found typescript tests and in integration.test.ts.

JSON schema generation

You can generate json schema from your TS types by using typed-fastify-schema or tfs bins

npx tfs gen
tfs gen [files]

Generates json schemas next to corresponding ts files

Positionals:
  files  glob pattern of files                               [string] [required]

Options:
  --help     Show help                                                 [boolean]
  --version  Show version number                                       [boolean]
# it will generate example_schema.gen.json next to file
npx tfs gen example_schema.ts

When schema is generated - just pass it to plugin to have runtime validations ๐ŸŽ‰

import jsonSchema from './example_schema.gen.json';

// ...

addSchema(app, {
  jsonSchema,
  service,
});

Writing service

  1. Handlers in one object Type inference will work nicely in this case, you just make TS happy and things are working ๐Ÿฅณ

  2. Handlers in a different file or separate functions - you will need to hint TS with exact type of handler.

import { RequestHandler, Schema } from '@coobaha/typed-fastify';

interface MySchema extends Schema {}

const myHandler: RequestHandler<MySchema, 'GET /hello'>['AsRoute'] = (req, reply) => {};
  1. When you want to have complex shared handler for multiple endpoints that intersect (share same props)
import { RequestHandler, Schema } from '@coobaha/typed-fastify';

interface MySchema extends Schema {}

const myHandlers: RequestHandler<MySchema, 'GET /hello' | `GET /hello2`>['AsRoute'] = (req, reply) => {};
  1. Sometimes properties won't be the same (for instance GET never has body and POST will). In this case you will probably be asked to add types to function params
import { RequestHandler, Schema } from '@coobaha/typed-fastify';

interface MySchema extends Schema {}

type MyHandlers = RequestHandler<MySchema, 'GET /hello' | `POST /hello`>;
const myHandlers = (req: MyHandlers['Request'], reply: MyHandlers['Reply']): MyHandlers['Return'] => {};

// if handler is async/await
const myHandlersAsync = async (req: MyHandlers['Request'], reply: MyHandlers['Reply']): MyHandlers['ReturnAsync'] => {};

addSchema(app, {
  jsonSchema: {},
  service: {
    'GET /hello': myHandlers,
    'GET /hello2': myHandlers,
  },
});

It might be that TS can't infer exact type of complex handler when passed to addSchema so you'll need to do it manually

addSchema(app, {
  jsonSchema: {},
  service: {
    'GET /hello': myHandlers,
    'GET /hello2': myHandlers as RequestHandler<ExtendedSchema, 'GET /hello2'>['AsRoute'],
  },
});

Annotating types

This library is using typescript-json-schema with custom transforms for schema generation. All @tjs annotations can be used to fine-tune schema output

  • @type can be used to specify end type after using toJSON, toString methods of objects like ObjectID from MongoDB

  • since we use typejescript-json-schema: all known limitations are also inherited: - Records are not transformed correctly, use { [k: string]: string } instead or hint with @tjs

  • additionalProperties are set to false by default

typed-fastify's People

Contributors

abahernest avatar coobaha avatar dependabot[bot] avatar github-actions[bot] avatar qwelias avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

typed-fastify's Issues

Is it possible to use typed-fastify with fastify-swagger?

addSchema(app, {
  // schema was generated
  jsonSchema: schema,
  service: exampleService,
});

// is it possible for fastify-swagger to consume generated types or there could be another way?
app.register(require("fastify-swagger"), {
  exposeRoute: true,
  routePrefix: "/docs",
  swagger: {
    info: { title: "fastify-api" },
  },
});

JSON type mismatch

As per standard JSON.stringify uses toJSON method on objects when possible to get JSON representation, unfortunately TS does not really have any built-ins to respect that behavior.

In typed-fastify it causes problems in cases where response type has any members with special toJSON implementation.
mongodb/ObjectId as an example, so generated JSONSchema has these members as type object (because ObjectId is an object), but when JSON.strigifyed it becomes a string, so response validation in fastify fails.

I'm not sure what's the best approach here would be, but I have a suggestion:

  • there are 3rd party solutions to base TS problem, such as type-fest/Jsonify
  • when generating a schema the types passed into it should be Jsonifyed, so that jenerated JSONSchema accounts for any transformations (should probably be fixed in typescript-json-schema)
  • should not affect reply.status().send(), but optionally it may make sense to allow it to take types compatible with Jsonifyed type

Would it be possible to infer the params from the URL?

Hey, thanks so much for this fantastic plugin! It's really helping in my project in keeping frontend and backend devs together.

I would love to take this even further and I have asked the following question on SO:

I have found this fantastic fastify plugin (https://github.com/Coobaha/typed-fastify) that uses the following syntax to infer the correct types passed to the Service, given a schema of type T.

import {Schema, Service} from '@coobaha/typed-fastify';


interface ExampleSchema extends Schema {
  paths: {
    'GET /test/:testId': {
      request: {
        params: {
          testId: {
            type: 'string',
          }
        };
      };
      // other properties, not relevant for question
    };
  };
}

const exampleService: Service<ExampleSchema> = {
  'GET /test/:testId': (req, reply) => {
    const testId = req.params.testId // this passes โœ…
    const testName = req.params.testName // this fails ๐Ÿ’” 
 
    // rest of handler
  }
};

// example is simplified. Find a full example here: https://github.com/Coobaha/typed-fastify

This plugin is already awesome, but I want to take it even further.

Given a /test/:testId string, we already know which parameters that URL needs. Having to specify it twice (in the URL and the params object), it seems to me something worth trying to automate in order to guarantee they always stay in sync.

I would love to find a way to automatically add the params type, computing it from the key string (the endpoint URL). For example, the key /test/:testId should have a params of type {testId: string}.

Imagine doing:

import {Schema, Service} from '@coobaha/typed-fastify';


interface ExampleSchema extends Schema {
  paths: {
    'GET /test/:testId': {
       // only other properties, no need for explicit params
    };
  };
}

// and still have the same type-checks on the params ๐Ÿคฏ:
const exampleService: Service<ExampleSchema> = {
  'GET /test/:testId': (req, reply) => {
    const testId = req.params.testId // this passes โœ…
    const testName = req.params.testName // this fails ๐Ÿ’” 
 
    // rest of handler
  }
};

In my understanding, this would be impossible with default typescript features and possibly only with a plugin to use at compile time using ttypescript.

Is there any way I can avoid using alternative compilers and writing my own plugins?

Thanks for any support

If I find a way, I would open a PR. I'm sure it will be handy :)

CLI prints confusing help text

Help prints generated filename

> tfs src/**.schema.ts

gen.bin.js <command>

Commands:
  gen.bin.js gen [files]  Generates json schemas next to
                          corresponding ts files

Should print command name instead

fix undefined values in objects

regression for undefined values in objects. I think it should make Jsonlike key optional, like:

type Obj = {
  a: string | null | undefined
}

type ResultJsonLikeType = {
 a?: string | null
}
declare const obj: Obj;

// works as JSON.strinigfy(obj) will remove undefined values
req.send(obj) // results in a {} response

Thanks :)

Hey I know this isn't quite an issue but I just wanted to say "Thank you"!

This is a pretty cool project and it really gives Fastify a first-class feeling when it comes to Typescript. With a little bit of funkiness I've been able to use this to share declaration files between backend and frontend, all thanks to you!

Keep developing, you're onto something. Expect a PR from me over the next few weeks for sure ๐Ÿ‘

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.