fastify-sarah v0.0.1-alpha.12
Fastify Sarah
A tiny dependencies injection framework for Fastify without addition runtime. Decorators base configuration. Create custom decorators made easy. Similar to NestJS but much simpler.
Installation
Manually
Create a folder a make it your current working directory:
mkdir <your-app>
cd <your-app>Initialize node project and install dependencies
npm init -y
npm install --save fastify fastify-sarah reflect-metadataSetup development environment
Install development dependencies
npm install -D ts-node-dev typescript @types/nodeAdd tsconfig.json for typescript with following options
// tsconfig.json
{
"compilerOptions": {
"target": "ES2016",
"module": "CommonJS",
"moduleResolution": "Node",
"outDir": "./dist",
"esModuleInterop": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}Make sure experimentalDecorators and emitDecoratorMetadata is always true. Because fastify-sarah is all about decorators.
ts-node-dev will watch your files and auto reload it. In package.json file add scripts
// package.json
{
...
"scripts": {
"dev": "ts-node-dev ./src/main.ts",
"prestart": "npm run build",
"start": "node ./dist/main",
"build": "tsc"
}
...
}:warning:
ts-node-devwill push nortification each time your code reload. to turn it off change"dev"to"ts-node-dev --no-notify ./src/main.ts"
Getting start
First, let's make a controller. Controller is a class, and class methods can be fastify route. Controller work as a group of routes, you can configure multiple routes by just configure a controller.
Create a message controller which serve a single welcome message.
// src/message.controller.ts
import { Controller, Route } from "fastify-sarah";
@Controller()
export class MessageController {
@Route("GET")
message() {
return "hello, world!";
}
}Here on this file, you created class MessageController with message method. To turn this class and method into controller and route, add @Controller() decorator to the class, and @Route() to the method. @Route required a route method such as GET, POST, PUT, DELETE, you can specify route url by second argument, and more fastify route options on third argument. Learn more about decorators, fastify route options.
Now that we had a controller, you need to register bootstrap it for be able to use. On main.ts file
// src/main.ts
import "reflect-metadata";
import Fastify from "fastify";
import { bootstrap } from "fastify-sarah";
import { MessageController } from "./message.controller";
const fastify = Fastify();
fastify.register(bootstrap, {
controllers: [MessageController],
});
fastify.listen(3000).then(() => console.log("Server is ready"));Import your MessageController class, initialize your fastify instance and then register bootstrap like a fastify plugin. Add your controllers to plugin options properties controller. It's array so you can have multiples controllers boot as one.
:warning: The
reflect-metadatapolyfill should be imported only once in your entire application
Now start the server by
npm run devCheck your browser http://localhost:3000 and you will see your hello, world! message.
Add another routes
Let's add one more routes to MessageController to send information about resource Message. On MessageController class add
// src/message.controller.ts
...
@Controller()
export class MessageController {
information = {
resouce: "Message",
description: "...",
version: "v0.0.1"
}
...
@Route("GET", "info")
info() {
return this.information;
}
}Noticed that we add information properties for the class and info method with @Route() decorator. The new @Route("info") specify url info. Now check browser again on url http://localhost:3000/info you will see your information object defined in MessageController class properties.
Let's switch all own resource about message to new url prefix. On MessageController, add your prefix url to @Controller() decorator.
...
@Controller("message")
export class MessageController {
...
}Now your simple hello, world! message route move to http://localhost:3000/message, and resource information now move to http://localhost:3000/message/info.
This is greate, now you can imagine configure multiple routes logic with a single controller. And with decorators you can reuse a same logic on difference routes.. For examples, authorization for a group of routes can access only by admin user, or publish and unpublish resource. Learn more about this on custom decorator and authentication example.
Make a CRUD resource
Let's build a RESTFul API with CRUD store in memory. First intall another dependencies for own life easier
npm install --save http-errors
npm install -D @types/http-errorsThis package contain some useful http errors. Now, remove all stuff in your MessageController class and add these lines into it
// src/message.controller.ts
import { Controller, Route } from "fastify-sarah";
import { FastifyRequest } from "fastify";
import { randomBytes } from "crypto";
import { NotFound } from "http-errors";
interface Message {
id: string;
text: string;
}
@Controller("message")
export class MessageController {
messages: { [key: string]: Message } = {};
// GET /message
@Route("GET")
all() {
// turn object into array and reply
return Object.values(this.messages);
}
// GET /message/:id
@Route("GET", ":id")
get(req: FastifyRequest) {
// get id string from request params
const id = req.params.id;
// return id exists in message object return it, else throw not found error
if (this.messages[id]) return this.messages[id];
else throw new NotFound(`message ${id} not exists`);
}
// POST /message
@Route("POST")
create(req: FastifyRequest) {
// get text from request body
const { text } = req.body;
// generate random id string with 5 characters
const id = randomBytes(5).toString("hex");
// make a new message
const message: Message = { id, text };
// store the new message into object
this.messages[id] = message;
// reply
return message;
}
// PUT /message/:id
@Route("PUT", ":id")
update(req: FastifyRequest) {
// reuse `this.get` to get request id
const message = this.get(req);
// get text from request body
const { text } = req.body;
// update text
message.text = text;
// reply
return message;
}
// DELETE /message/:id
@Route("DELETE", ":id")
delete(req: FastifyRequest) {
// reuse `this.get` to get request id
const message = this.get(req);
// delete message in the object
delete this.messages[message.id];
// reply
return message;
}
}Now test your REST API. Noticed that a single logic find a message by id if the id not exists throw NotFound error on GET /message route, can be reuse further on PUT /message/:id and DELETE /message/:id routes.
Add route schema
This is still not good enought. A good backend API always validate incoming request data and use the right http code for right the action. Fortunately, fastify has built in JSON Schema validate by ajv make things quite easier, Check out Fastify validation. To add schema into route on fastify-sarah, there are two ways.
1. Original way to define route JSON schema
Add json schema definition to route configure, by adding third argument into @Route(). For example own CRUD routes. Let's add few more JSON schema.
import { ServerResponse } from "http";
...
const MessageSchema = {
$id: "Message",
type: "object",
properties: {
id: { type: "string" },
text: { type: "string" },
},
};
const CreateMessageDtoSchema = {
$id: "CreateMessageDto",
type: "object",
properties: {
text: { type: "string", minLength: 4 },
},
required: ["text"],
};
...
class MessageController {
@Route("GET", "", {
schema: {
response: { 200: { type: "array", items: MessageSchema } },
},
})
all() {
...
}
...
@Route("POST", "", {
schema: {
body: CreateMessageDtoSchema,
response: { 201: MessageSchema },
},
})
create(req: FastifyRequest, rep: FastifyReply<ServerResponse>) {
...
rep.status(201).send(message);
}
...
}This way for configuring JSON Schema was greate. But it have alot of boilerplate and Typescript cannot recognize any type from body, query, params or return type. This is again the reason we are using Typescript to avoid typo. This problem can solve with type transform tools (JSON Schema to Typescript), but most of tools required extra step to generate your Type, and you have to seperact JSON Schema, Typescript Type and the actual code into multiple files. fastify-sarah try to fix this problem by decorators.
2. JSON schema decorator
Define json schema by decorator and class. This will provide benifit both reusable ability and typescript sugar.
:warning: Upcoming feature
How to get Fastify Instance
You can inject fastify instance from custom decorator, injectable class by FastifyInst token. Examples
On class
import { Inject, FastifyInst } from "fastify-sarah";
import { FastifyInstance } from "fastify";
@Controller()
export class MyController {
constructor(@Inject(FastifyInst) fastify: FastifyInstance) {}
}On custom decorator
import { makeDecorator, FastifyInst } from "fastify-sarah";
import { FastifyInstance } from "fastify";
export function MyDecorator() {
return makeDecorator({
on: "both",
callback: () => ({
deps: () => [FastifyInst], // specify inject FastifyInst token here
factory: (fastify: FastifyInstance /* fastify instance come here */) => {
...
},
}),
});
}5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago