1.1.0 • Published 3 years ago

fastify-boot v1.1.0

Weekly downloads
-
License
MIT
Repository
github
Last release
3 years ago

fastify-boot

Fastify Boot is a Spring Boot inspired opinionated view on building and bootstrapping a Fastify REST application. The goal of this project is to create a framework for developers to rapidly start feature rich new projects and abstract away the repetitive tasks like plumbing and configuration, so they can focus on the important things.

Some key features include:

  • Dependency injection.
  • Intuitive annotation based declaration of Fastify components.
  • Automatic resolution and bootstrapping of controllers, hooks and plugins.
  • Built in and configurable environment variable injection.
  • Built in code bundling with Webpack.
  • Built in Typescript support.
  • Built in Jest support.

Table of Contents

Setup

Install

The recommended way to create a fastify-boot app is with Create Fastify Boot. Based on the same concept as create-react-app, it will initialize a new fastify-boot project with all the dependencies and scaffolding required to get started.

$ npx create-fastify-boot {appName}

or

$ npm install -G create-fastify-boot
$ create-fastify-boot {appName}

Getting started

CLI

  • fastify-boot build: Compile your code with Webpack into a single bundle file.
  • fastify-boot start: Start your Fastify server.
  • fastify-boot test: Run Jest in interactive mode.

Check the readme file in the project generated by Create Fastify Boot for more information.

Dependency Injection

Under the hood fastify-boot extends the ts-injection library to provide dependency injection out of the box. Annotations like @Application,@Controller,@GlobalHookContainer and @PluginContainer are all extensions of @Injectable, so their instantiation is handled by the injection context.

Simply add @Injectable to classes that you want to inject into your fastify-boot components.

@Injectable
export class ServiceOne {
    constructor() {
    }

    public printMessage(): string {
        return "Hello from one!";
    }
}

@Injectable
export class ServiceTwo {
    constructor() {
    }

    public printMessage(): string {
        return "Hello from two!";
    }
}

@Controller("/greet")
export class GreetingController {
    @Autowire(ServiceTwo)
    private svcTwo: ServiceTwo;
    
    constructor(private svcOne: ServiceOne) {
        console.log(this.svcOne.printMessage());
        // Outputs: Hello from one
        console.log(this.svcTwo.printMessage());
        // Outputs: Hello from two
    }
}

The following functionality from ts-injection is exported for your consumption:

  • @Injectable
  • @Autowire
  • @Env
  • resolve()
  • register()

Please read the ts-injection documentation for additional information on how these methods and annotations can be used.

Configuration

Fastify Options

Define your Fastify instance options in the fastify.config.ts file location in your project's root directory.

const config: FastifyOptions = {
  logger: true,
};

export default config;

Environment variables

Defining

By default fastify-boot will look for a .env file in your project's root directory and load the values into process.env when you start your application, however you can configure a specific environment by providing an environment argument

E.g.

yarn start {env}

or

fastify-boot start {env}

Will look for .env.{env} instead of .env.

Consuming

Use the @Env annotation to easily read process.env from any class in your application. If the value you're reading isn't a string you can also provide a mapper.

// .env

MY_STRING=hello
MY_NUMBER=12345
// env.controller.ts

@Controller()
export class EnvController {
    @Env("MY_STRING")
    private myString: string;

    @Env<number>("MY_NUMBER", (val: string) => Number(val))
    private myNumber: number;
    
    constructor() {}

    @GetMapping("/get")
    public async getEnvVars(
        request: FastifyRequest,
        reply: FastifyReply
    ): Promise<void> {
        reply.status(200).send({
            myString: this.myString,
            myNumber: this.myNumber
        });
        // { myString: "hello", myNumber: 12345 }
    }
}

Application

This is the entry class to your Fastify app. Feel free to add additional bootstrapping or server setup here.

Fastify instance

The @FastifyServer annotation provides access to the underlying Fastify server for your consumption (this can be used in any class).

@FastifyApplication
export class App {
    @FastifyServer()
    private server: FastifyInstance;

    constructor() {
    }

    public start(): void {
        this.server.listen(8080, "0.0.0.0", (err) => {
            if (err) {
                console.error(err);
            }
        });
    }
}

Controllers

A controller in fastify-boot is a class marked with the @Controller annotation inside a file following the convention ${name}.controller.ts. Routes can be defined by marking methods with the generic @RequestMapping annotation or a specific request method annotation e.g. @GetMapping.

Controller Hooks

Hooks scoped to all the routes within this controller can be applied using the generic @Hook annotation or the relevant specific hook mapping e.g. @OnSend.

Route Hooks

If a hook should only be applied to one route in a controller, you can add it as part of the RouteOptions provided in @RequestMapping. Route hooks will overwrite controller hooks if both are provided for the same hook name.

// greeting.controller.ts

@Controller("/greet")
export class GreetingController {
    constructor(private service: Greeter) {
    }

    @RequestMapping({
        method: "GET",
        url: "/bonjour"
    })
    public async getBonjour(
        request: FastifyRequest,
        reply: FastifyReply
    ): Promise<void> {
        reply.status(200).send({greeting: this.service.sayBonjour()});
    }

    @GetMapping("/hello")
    public async getHello(
        request: FastifyRequest,
        reply: FastifyReply
    ): Promise<void> {
        reply.status(200).send({greeting: this.service.sayHello()});
    }

    @OnSend()
    public async addCorsHeaders(
        request: FastifyRequest,
        reply: FastifyReply,
        payload: any
    ): Promise<void> {
        // I will only apply to routes defined in GreetingController
        reply.headers({
            "Access-Control-Allow-Origin": "*",
        });
        return payload;
    }
}

The above code will result in two routes being created: /greet/bonjour and /greet/hello that both have the addCorsHeaders() method attached to their onSend hook.

Hooks

Global Hook Containers

Global hooks can be defined within the context of a class so that you have access to all the magic of fastify-boot dependency injection. The classes are marked with the @GlobalHookContainer annotation, and the file that contains the class must follow the naming convention ${name}.hook.ts.

Hooks are defined within the container by annotating methods with the generic @Hook annotation, or specific hook annotation e.g. @OnSend.

// myHooks.hook.ts

@GlobalHookContainer
export class MyHooks {
    @Env("ALLOW_ORIGIN")
    private allowOrigin: string;

    constructor() {
    }

    @Hook("preHandler")
    public async doSomething(
        request: FastifyRequest,
        reply: FastifyReply
    ): Promise<void> {
        // Do something with request
        return payload;
    }

    @OnSend()
    public async addCorsHeaders(
        request: FastifyRequest,
        reply: FastifyReply,
        payload: any
    ): Promise<void> {
        reply.headers({
            "Access-Control-Allow-Origin": this.allowOrigin,
        });
        return payload;
    }
}

Global Hook Functions

If your hook doesn't need access to a class instance, you can also define hooks in a functional manner. You still need to name it ${name}.hook.ts. The function name should be the name of the Fastify hook you want to add the method to.

// addCorsHeaders.hook.ts

export async function onSend(
    request: FastifyRequest,
    reply: FastifyReply,
    payload: any
): Promise<void> {
    reply.headers({
        "Access-Control-Allow-Origin": this.allowOrigin,
    });
    return payload;
}

Supported hooks

  • onRequest
  • preParsing
  • preValidation
  • preHandler
  • preSerialization
  • onError
  • onSend
  • onResponse
  • onTimeout

Plugins

Plugin Containers

Plugins can be defined within the context of a class so that you have access to DI. The classes are marked with the @PluginContainer annotation, and the file that contains the class must follow the naming convention ${name}.plugin.ts.

Plugins are defined in a plugin container by marking methods with the @PluginHandler annotation.

// myPlugin.plugin.ts

@PluginContainer
export class MyPlugin {
    constructor() {
    }

    @PluginHandler({
        myPlugin: {
            opt1: "Hello world"
        }
    })
    public async myPlugin(fastify: FastifyInstance,
                          opts: any,
                          done: () => void): Promise<void> {
        console.log(opts.myPlugin.opt1);
        // Outputs: Hello world
        // Do stuff with Fastify instance
        done();
    }
}

Plugin Objects

If your plugin doesn't need access to a class instance, or you're importing from an external package, you can also define a plugin as an object. You still need to put the object inside a file named ${name}.plugin.ts.

// myPlugin.plugin.ts

export const externalPlugin: PluginObject = {
    plugin: require("external-plugin")
}

export const myPlugin: PluginObject = {
    plugin: (fastify: FastifyInstance,
             opts: any,
             done: () => void): Promise<void> => {
        console.log(opts.myPlugin.opt1);
        done()
    },
    opts: {
        myPlugin: {
            opt1: "Hello world"
        }
    }
}

API Reference

Application

@FastifyApplication

Apply this annotation to the main entry class of your application only. Indicates the entry point of your server.

@FastifyServer()

Injects the Fastify server instance into a class member for consumption. Can be used in any class.

Controllers

@Controller(basePath?: string)

Indicates to the framework that methods within this class should be scanned for route handlers. An optional basePath can be provided which prefixes the url of all routes defined in the controller.

@RequestMapping(options: RequestOptions)

Define a generic route within a controller. You must supply the request options including things like method, url etc. The options object is a Fastify RouteOptions interface, minus a handler field.

Implicit routes

You can define implicit route methods using:

  • @GetMapping(url: string, options?: ImplicitRequestOptions)
  • @PostMapping(url: string, options?: ImplicitRequestOptions)
  • @PutMapping(url: string, options?: ImplicitRequestOptions)
  • @DeleteMapping(url: string, options?: ImplicitRequestOptions)
  • @PatchMapping(url: string, options?: ImplicitRequestOptions)
  • @HeadMapping(url: string, options?: ImplicitRequestOptions)
  • @OptionsMapping(url: string, options?: ImplicitRequestOptions)

Hooks

@GlobalHookContainer

Indicates to the framework that methods within this class should be scanned for global hook methods. These hooks will be applied to every request on the server.

@Hook(hookName: string)

Define a generic hook method, provide the name of the hook the method should be applied to.

  • When applied to a @GlobalHookContainer class, the hook is applied at the server level.
  • When applied to a method within a @Controller class, the hook is applied to routes within that controller.
Implicit hooks
  • @OnError
  • @OnRequest
  • @OnResponse
  • @OnSend
  • @OnTimeout
  • @PreHandler
  • @PreParsing
  • @PreSerialization
  • @PreValidation

Plugins

@PluginContainer

Indicates to the framework that methods within this class should be scanned for plugin handlers.

@PluginHandler(options?: FastifyPluginOptions)

Defines a Fastify plugin handler when used within a @PluginController class.

The future

Cross platform

Fastify boot was developed on OSX. I'm not sure if all the build scripts will work correctly on Linux or Windows machines - I haven't had the chance to test it out yet. If you are running into any issues, please raise an issue.

Potential features

  • Annotations for user authentication and defining access control on routes.
  • Common framework for error handling/API responses.

If you have a feature you'd like to see, drop me an email: me@tylerburke.dev.