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.
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}
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.
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.
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;
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
.
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 }
}
}
This is the entry class to your Fastify app. Feel free to add additional bootstrapping or server setup here.
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);
}
});
}
}
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
.
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
.
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.
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;
}
}
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;
}
- onRequest
- preParsing
- preValidation
- preHandler
- preSerialization
- onError
- onSend
- onResponse
- onTimeout
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();
}
}
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"
}
}
}
Apply this annotation to the main entry class of your application only. Indicates the entry point of your server.
Injects the Fastify server instance into a class member for consumption. Can be used in any class.
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.
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.
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)
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.
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.
@OnError
@OnRequest
@OnResponse
@OnSend
@OnTimeout
@PreHandler
@PreParsing
@PreSerialization
@PreValidation
Indicates to the framework that methods within this class should be scanned for plugin handlers.
Defines a Fastify plugin handler when used within a @PluginController
class.
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.
- 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
.