@memberid/fastify-di v0.4.2
Simple Fastify & TypeScript Dependency Injection
Why use dependency injection?
- Helps in Unit testing. (See this).
- Boiler plate code is reduced.
- Extending the application becomes easier.
Check this article: A quick intro to Dependency Injection.
This package use fastify
for http server -- one of the fastest web frameworks in town (check this benchmark), typeorm for database ORM. Dependency injection is inspired by typedi. File naming is inspired by Nest. Decorator is inspired by fastify-decorator.
Getting Started
- Folder Structure
- Configuration
- Installation
- Create simple controller
- Create entity
- Create simple service
- Create schema
- Create controller that inject a service
- Create simple plugin
Testing Service & Controller
Folder Structure
.
├── package.json
├── src
│ ├── main.ts
│ └── services
│ ├── hello
│ │ └── hello.controller.ts
│ └── user
│ ├── user.controller.ts
│ ├── user.entity.ts
│ ├── user.schema.ts
│ └── user.service.ts
├── tsconfig.build.json
└── tsconfig.json
Configuration
- This boilerplate use
dotenv
package. Please check.env
file to change configuration.
Installation
Install
npm install @memberid/fastify-di
Create Simple Controller
Controller is class that create router and it's wrapper. You can create a wrapper by defining a new class and mark it with @Controller()
and *.controller.ts
file extension. You can create a router by defining a new function and mark it with @Get() or @Post()
.
// file hello.controller.ts
import { Controller, Get } from '@memberid/fastify-di'
@Controller()
export class HelloController {
@Get()
sayHello (): string {
return 'hello'
}
}
You can add fastify plugin options or fastify route options to a controller or a router as its parameter. (See this)
This package Dependency Injection (DI) mechanism will load all controllers, routes, services, and register them to fastify instance. And after this, you can just run your server by create this file:
//file main.ts
import { createServer, start } from '@memberid/fastify-di'
import path from 'path'
const options = { logger: false }
const targetDir = path.join(__dirname)
createServer(options, targetDir).then(server => {
start(server)
})
And run (please check package.json
first):
$ npm run build
$ npm start
Create Entity
Entity is a class that maps to a database table. You can create an entity by defining a new class and mark it with @Entity()
and *.entity.ts
file extension. Check this for more docs.
// file user.entity.ts
import { Entity, Column, Index } from 'typeorm'
import { BasicEntity } from '@memberid/fastify-di'
@Entity()
@Index(['id', 'email', 'username'])
export class User extends BasicEntity {
@Column({ unique: true })
email: string
@Column({ unique: true })
username?: string
@Column()
password?: string
}
Create Simple Service
Service is class that have access directly to database. You can create a service by defining a new class and mark it with @Service()
and *.service.ts
file extension.
// file: user.service.ts
import { BasicService, Service } from '@memberid/fastify-di'
import { User } from './user.entity'
@Service()
export class UserService extends BasicService {
getName (): string {
return 'Captain America'
}
public async register (payload: User): Promise<User> {
try {
const user = new User()
if (!payload.email) throw new Error('Email empty')
if (!payload.username) throw new Error('Username empty')
if (!payload.password) throw new Error('Password empty')
user.email = payload.email
user.username = payload.username
user.password = payload.password
return this.repo(User).save(user)
} catch (error) {
throw this.err('USER_REGISTER_ERROR', error)
}
}
public async getAllUser (): Promise<User[]> {
try {
return this.repo(User).find({
select: ['id', 'username', 'email']
})
} catch (error) {
throw this.err('GET_ALL_USER_ERROR', error)
}
}
}
Create Schema
Fastify uses a schema-based approach, and even if it is not mandatory we recommend using JSON Schema to validate your routes and serialize your outputs. Internally, Fastify compiles the schema into a highly performant function. See this.
// file user.schema.ts
export const getAllUserSchema = {
response: {
200: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'string' },
username: { type: 'string' },
email: { type: 'string' }
}
}
}
}
}
Create controller that inject a service
// file: user.controller.ts
import { Controller, InjectService, Get, Post } from '@memberid/fastify-di'
import { UserService } from './user.service'
import { User } from './user.entity'
import { FastifyRequest, FastifyReply } from 'fastify'
import { Http2ServerResponse } from 'http2'
import { getAllUserSchema } from './user.schema'
@Controller({ prefix: 'user' })
export class UserController {
@InjectService(UserService)
userService: UserService
@Get({ schema: getAllUserSchema })
async getAll (): Promise<User[]> {
const users = await this.userService.getAllUser()
return users
}
@Post({ url: '/' })
async register (request: FastifyRequest, reply: FastifyReply<Http2ServerResponse>): Promise<void> {
const payload = request.body
const user = await this.userService.register(payload)
reply.send(user)
}
}
Create Simple Plugin
You can create a new fastify plugin by just copy this template and paste to new file with *.plugin.ts
extension. Please notify that you don't neeed fastify-plugin
on this creation. Server will pass your plugin to it automatically.
// file support.plugin.ts
import { FastifyInstance } from 'fastify'
export const plugin = function (fastify: FastifyInstance, opts: any, next: Function): void {
fastify.decorate('someSupport', function () {
return 'hugs'
})
next()
}
Testing Folder structure
- folder name:
__test__
- file extension:
*.spec.ts
.
├── babel.config.js
├── jest.config.js
├── package.json
├── src
│ ├── @types
│ │ └── index.d.ts
│ ├── main.ts
│ ├── plugins
│ │ ├── __tests__
│ │ │ └── support.plugin.spec.ts
│ │ └── support.plugin.ts
│ └── services
│ ├── hello
│ │ ├── __tests__
│ │ │ └── hello.controller.spec.ts
│ │ └── hello.controller.ts
│ └── user
│ ├── __tests__
│ │ ├── user.controller.spec.ts
│ │ └── user.service.spec.ts
│ ├── user.controller.ts
│ ├── user.entity.ts
│ ├── user.schema.ts
│ └── user.service.ts
├── tsconfig.build.json
└── tsconfig.json
Simple Controller Testing
// file hello.controller.spec.ts
import { FastifyInstance } from 'fastify'
import { createServer } from '@memberid/fastify-di'
import path from 'path'
const targetDir = path.join(__dirname, '../../')
let server: FastifyInstance
beforeAll(async () => {
server = await createServer({ logger: false }, targetDir)
})
afterAll(() => {
server.close()
})
describe('simple test', () => {
test('/', async done => {
const result = await server.inject({
url: '/',
method: 'GET'
})
// console.log(result.payload)
expect(result.payload).toBe('hello')
done()
})
})
Service Testing
// file user.service.spec.ts
import { createConnection, loader, serviceContainer } from '@memberid/fastify-di'
import { UserService } from '../user.service'
import path from 'path'
const targetDir = path.join(__dirname, '../../')
const entityFiles = `${targetDir}/**/**/*.entity.*s`
let service: UserService
beforeAll(async () => {
await createConnection(entityFiles)
await loader(targetDir)
service = serviceContainer.get('UserService')
service.deleteAll()
})
afterAll(() => {
service.close()
})
describe('user service', () => {
test('get all', async done => {
const users = await service.getAllUser()
expect(users.length).toBe(0)
done()
})
test('add user', async done => {
const payload = {
email: 'pram2016@gmail.com',
username: 'zaid',
password: 'secret'
}
const users = await service.register(payload)
expect(users.username).toBe('zaid')
done()
})
test('get all', async done => {
const users = await service.getAllUser()
expect(users.length).not.toBe(0)
done()
})
})
Controller Testing
// file user.controller.spec.ts
import { FastifyInstance } from 'fastify'
import { createServer, serviceContainer } from '@memberid/fastify-di'
import { UserService } from '../user.service'
import path from 'path'
const targetDir = path.join(__dirname, '../../')
let server: FastifyInstance
let service: UserService
beforeAll(async () => {
server = await createServer({ logger: false }, targetDir)
service = serviceContainer.get('UserService')
service.deleteAll()
})
afterAll(() => {
server.close()
})
test('GET /user', async done => {
const result = await server.inject({
url: '/user',
method: 'GET'
})
done()
})
test('POST /user', async done => {
const result = await server.inject({
url: '/user',
method: 'POST',
payload: {
email: 'pram@diversa.id',
username: 'zaid',
password: 'secret'
}
})
expect(result.statusCode).toBe(200)
done()
})
4 years ago