0.4.2 • Published 4 years ago

@memberid/fastify-di v0.4.2

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

Simple Fastify & TypeScript Dependency Injection

js-standard-style build

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

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()
})