koa + typescript

Typescript + tsLint normalizes code that selects TypeOrM as the ORM of the database and Log4JS as the output log

1. Directory structure

├─ bin │ ├─ www.ts// Start the application├ ─ ─ logs// Store logs├─ SRC │ ├─ app │ ├─ Components// Controller components│ │ │ ├ ─ ─ the account │ │ │ │ ├ ─ ─ the controller. The ts │ │ │ │ └ ─ ─ the service. The ts │ │ │ └ ─ ─ the user │ │ │ ├ ─ ─ ├─ 07.06.constants/ / constant│ ├─ ├─ ├─ exercises// Some core class or globally referenced methods│ │ │ ├ ─ ─ error. Ts │ │ │ └ ─ ─ logger. Ts │ │ ├ ─ ─ the database// Database connection│ ├─ ├─ exercises/ / entity│ │ │ └ ─ ─ the user │ │ │ ├ ─ ─ the user. The entity. The ts │ │ │ └ ─ ─ the user. The model. The ts │ │ ├ ─ ─ middleware/ / middleware│ │ │ ├ ─ ─ error. Middleware. Ts │ │ │ ├ ─ ─ JWT, middleware, ts │ │ │ ├ ─ ─ logger. The middleware. The ts │ │ │ └ ─ ─ │ ├ ─ garbage. Response. Middleware// Utility functions│ ├ ─ ├ ─ garbage, ├ ─ garbage// Apply the startup class│ └ ─ ─ environments// Multi-environment configuration│ │ ├ ─ ─ env. Dev. Ts ├ ─ ─ env. Prop. Ts │ └ ─ ─ but ts ├ ─ ─ nodemon. Json// Nodemon configuration, watch TS file├─ Package-lock. json ├─ Package. json ├─ tsconfig.json ├.txtCopy the code

2. package.json

{
  "name": "huzz-koa-template"."version": "1.0.0"."description": ""."main": "index.js"."dependencies": {
    "@koa/cors": "^ 3.0.0"."koa": "^ 2.11.0"."koa-body": "^ 4.4.1"."koa-jwt": "^ 3.6.0"."koa-route-decors": "^ 1.0.3"."koa-router": "^ 7.4.0"."koa-static": "^ 5.0.0"."log4js": "^ 5.3.0." "."mysql2": "^ 2.0.0." "."reflect-metadata": "^ 0.1.13"."typeorm": "^ 0.2.20"
  },
  "devDependencies": {
    "@types/jsonwebtoken": "^ 8.3.5"."@types/koa": "^ 2.0.52"."@types/koa-router": "^ 7.0.42"."@types/koa-static": "^ 4.0.1." "."@types/koa__cors": "^ 2.2.3"."cross-env": "^ 6.0.3"."nodemon": "^ 1.19.4"."ts-node": "^ 8.5.0"."tslint": "^ 5.20.1"."tslint-config-standard": "^ 9.0.0"."typescript": "^ 3.7.2." "
  },
  "scripts": {
    "dev": "cross-env NODE_ENV=development nodemon --config nodemon.json"."compile": "tsc"."start": "npm run compile && pm2 start ./bin/www --name app"."restart": "npm run compile && pm2 start ./dist/app/app.js"."stop": "pm2 stop app"
  },
  "repository": {
    "type": "git"."url": "git+https://github.com/xhuz/huzz-koa-template.git"
  },
  "keywords": []."author": ""."license": "ISC"."bugs": {
    "url": "https://github.com/xhuz/huzz-koa-template/issues"
  },
  "homepage": "https://github.com/xhuz/huzz-koa-template#readme"
}

Copy the code

3. Start

1. core

  1. Custom error,
    // error.ts
    // From the native Error class, easy to throw all kinds of exceptions
    export class CustomError extends Error { 
      code: number;
      constructor(code: number, message: string) {
        super(message);
        this.code = code; }}Copy the code
  2. To configure the logger
    // Logger. ts see the log4js documentation for details
    import {configure, getLogger} from 'log4js';
    import {resolve} from 'path';
    import {Context} from 'koa';
    
    const logPath = resolve(__dirname, '.. /.. /.. /logs'); // Log file path. Ensure that the path exists
    
    configure({
      appenders: {
        console: {type: 'console'},
        dateFile: {type: 'dateFile'.filename: `${logPath}/log.log`.pattern: 'yyyy-MM-dd'.alwaysIncludePattern: true.keepFileExt: true}},categories: {
        default: {
          appenders: ['console'.'dateFile'].level: 'info'
        },
        mysql: {
          appenders: ['console'.'dateFile'].level: 'info'}}});export const logger = getLogger('default');
    export const mysqlLogger = getLogger('mysql');
    
    export function logText(ctx: Context, ms: number) {
      const remoteAddress = ctx.headers['x-forwarded-for'] || ctx.ip || ctx.ips || (ctx.socket && ctx.socket.remoteAddress);
      return `${ctx.method} ${ctx.status} ${ctx.url} - ${remoteAddress} - ${ms}ms`;
    }
    
    Copy the code

2. database

Configuring the Database

import {createConnection, ConnectionOptions, Logger, QueryRunner} from 'typeorm';
import {environment} from '.. /.. /environments'; // Support automatic import of different configurations from multiple environments. For details, see Environments
import {mysqlLogger} from '.. /core/logger';

// Export the connection database function
export function connection() {
  const config: ConnectionOptions = environment.db as any;
  Object.assign(config, {logger: new DbLogger()});
  createConnection(config).then(() = > {
    console.log('mysql connect success');
  }).catch(err= > {
    mysqlLogger.error(err);
  });
}

// Take over the Typeorm Logger with our own logger
class DbLogger implements Logger {

  logQuery(query: string, parameters? : any[], queryRunner? : QueryRunner) {
    mysqlLogger.info(query);
  }

  logQueryError(error: string, query: string, parameters? : any[], queryRunner? : QueryRunner) {
    mysqlLogger.error(query, error);
  }

  logQuerySlow(time: number, query: string, parameters? : any[], queryRunner? : QueryRunner) {
    mysqlLogger.info(query, time);
  }

  logSchemaBuild(message: string, queryRunner? : QueryRunner) {
    mysqlLogger.info(message);
  }

  logMigration(message: string, queryRunner? : QueryRunner) {
    mysqlLogger.info(message);
  }

  log(level: 'log' | 'info' | 'warn', message: any, queryRunner? : QueryRunner) {
    switch (level) {
      case 'info': {
        mysqlLogger.info(message);
        break;
      }
      case 'warn': { mysqlLogger.warn(message); }}}}Copy the code

3. environments

Multi-environment Configuration

// env.dev.ts
import {ConnectionOptions} from 'typeorm';

export const db: ConnectionOptions = {
  type: 'mysql'.host: 'localhost'.port: 3307.username: 'root'.password: '123456'.database: 'test'.logging: true.// synchronize: true,
  timezone: '+ 08:00'.dateStrings: true.entities: ['.. /**/*.entity.ts']};Copy the code
// env.prod.ts
import {ConnectionOptions} from 'typeorm';

export const db: ConnectionOptions = {
  type: 'mysql'.host: 'localhost'.port: 3306.username: 'root'.password: '123456'.database: 'test'.logging: true.timezone: '+ 08:00'.dateStrings: true.entities: ['.. /**/*.entity.ts']};Copy the code
// index.ts Exports the configuration according to different environments. If you need other environments, you can add environment files and modify the following code
import * as dev from './env.dev';
import * as prop from './env.prop';

const env = process.env.NODE_ENV;

let environment = dev;

if(env ! = ='development') {
  environment = prop;
}

export {environment};
Copy the code

4. utils

// crypto. Ts
import * as Crypto from 'crypto';

export function cryptoPassword(pwd: string, key: string) {
  return Crypto.createHmac('sha256', key).update(pwd).digest('hex');
}
Copy the code

5. constants

// index.ts
export const JWT_SECRET = 'huzz-koa-server';  / / JWT secret key
export const NO_AUTH_PATH = {
  path: [/ / / /./\/register/./\/login/] // Public interface, routes excluded by JWT
};
Copy the code

6. middleware

  1. Middleware that defines error handling
    // error.middleware.ts
    import {Context} from 'koa';
    import {logger} from '.. /core/logger';
    
    export async function errorHandle(ctx: Context, next: () => Promise<any>) {
      try {
        await next();
      } catch (err) {
        if(! err.code) { logger.error(err.stack); } ctx.body = {code: err.code || -1.message: err.message.trim()
        };
        ctx.status = 200; // Set the HTTP status code to 200 so that the front end does not report errors}}Copy the code
  2. Middleware that defines HTTP request responses and harmonizes response formats
    // resoponse.middleware.ts
    import {Context} from 'koa';
    
    export async function responseHandle(ctx: Context, next: () => Promise<any>) {
      if(ctx.result ! = =undefined) {
        ctx.type = 'json';
        ctx.body = {
          code: 0.data: ctx.result,
          message: 'ok'
        };
      }
      await next();
    }
    Copy the code
  3. Define Logger middleware to log HTTP requests
    // logger.middleware.ts
    import {Context} from 'koa';
    import {logText, logger} from '.. /core/logger';
    
    export async function loggerHandle(ctx: Context, next: () => Promise<any>) {
      const start = Date.now();
      await next();
      const end = Date.now();
      const ms = end - start;
      const log = logText(ctx, ms);
      logger.info(log);
    }
    Copy the code
  4. Define the middleware for JWT validation
    import * as koaJwt from 'koa-jwt';
    import {JWT_SECRET, NO_AUTH_PATH} from '.. /constants';
    
    export const jwt = koaJwt({
      secret: JWT_SECRET
    }).unless(NO_AUTH_PATH); // Exclude public routes
    
    Copy the code

7. entities

Database entity

// user/user.entity.ts Creates a user entity
import {Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn} from 'typeorm';

@Entity()
export class User { @PrimaryGeneratedColumn() id! : number;// add non-null assertion "!" in ts strict mode@Column() username! : string; @Column() password! : string; @Column() nickname! : string; @CreateDateColumn() createTime! :Date; @UpdateDateColumn() updateTime! :Date;
}

// user/user.model.ts Creates the entity model
import {User} from './user.entity';
import {getRepository, Repository} from 'typeorm';
import {cryptoPassword} from '.. /.. /utils/crypto';
import {Injectable} from 'koa-route-decors'; // Imports the Injectable decorator, declaring that the class can be injected

@Injectable()
export class UserModel {
  private repository: Repository<User>;
  private select: (keyof User)[] = ['id'.'username'.'nickname'];

  constructor() {
    this.repository = getRepository(User);
  }

  async create(user: User) {
    const result = await this.repository.save(user);
    return result;
  }

  async findById(id: number) {
    const user = await this.repository.findOne(id, {select: this.select});
    return user;
  }

  async findByUsername(username: string) {
    const user = await this.repository.findOne({username}, {select: this.select});
    return user;
  }

  async findAndCheckPassword(username: string, password: string) {
    const user = await this.repository.findOne({username, password: cryptoPassword(password, username)}, {select: this.select});
    return user;
  }

  async findAll() {
    const users = await this.repository.find({select: ['id'.'username'.'nickname']});
    returnusers; }}Copy the code

8. Components, handle HTTP requests

  1. Account component,
    // Account.controller.ts Controller, which handles HTTP logic
    import {Context} from 'koa';
    import {Post, Controller} from 'koa-route-decors';
    import * as jwt from 'jsonwebtoken';
    import {JWT_SECRET} from '.. /.. /constants';
    import {AccountService} from './account.service';
    
    @Controller()
    export class AccountController {
      constructor(private accountService: AccountService) {}
      @Post()
      async register(ctx: Context, next: () => Promise<any>) {
        const {username, password, nickname} = ctx.request.body;
        const result = await this.accountService.insert(username, password, nickname);
        ctx.result = {
          id: result.id,
          username: result.username,
          nickname: result.nickname
        };
        await next();
      }
    
      @Post()
      async login(ctx: Context, next: () => Promise<any>) {
        const {username, password} = ctx.request.body;
        // Verify password and generate token
        const user = await this.accountService.verifyPassword(username, password);
        const token = jwt.sign({username: user.username, id: user.id}, JWT_SECRET, {expiresIn: '30d'});
        ctx.result = {
          id: user.id,
          username: user.username,
          nickname: user.nickname,
          token
        };
        awaitnext(); }}// account.service.ts handles entity model-related logic
    import {UserModel} from '.. /.. /entities/user/user.model';
    import {CustomError} from '.. /.. /core/error';
    import {User} from '.. /.. /entities/user/user.entity';
    import {cryptoPassword} from '.. /.. /utils/crypto';
    import {Injectable} from 'koa-route-decors';
    
    @Injectable()
    export class AccountService {
      constructor(private userModel: UserModel) {}
    
      async insert(username: string, password: string, nickname: string = ' ') {
        const exist = await this.userModel.findByUsername(username);
        if (exist) {
          throw new CustomError(-1.'User already exists');
        }
        const user = new User();
        user.username = username;
        user.password = cryptoPassword(password, username);
        user.nickname = nickname;
        const result = await this.userModel.create(user);
        return result;
      }
    
      async verifyPassword(username: string, password: string) {
        const user = await this.userModel.findAndCheckPassword(username, password);
        if (user) {
          return user;
        } else {
          throw new CustomError(-1.'Wrong username or password'); }}}Copy the code
  2. The user user component, similar to the account component

Other HTTP-related logic extends components

9. Apply startup classes

// app.ts
import 'reflect-metadata';
import * as Koa from 'koa';
import * as cors from '@koa/cors';
import * as body from 'koa-body';
import * as staticService from 'koa-static';
import * as Router from 'koa-router';
import {autoRouter} from 'koa-route-decors';
import {loggerHandle} from './middleware/logger.middleware';
import {errorHandle} from './middleware/error.middleware';
import {responseHandle} from './middleware/response.middleware';
import {jwt} from './middleware/jwt.middleware';
import {resolve} from 'path';
import {connection} from './database';

export class App {
  private app: Koa;
  constructor() {
    this.app = new Koa();
    this.init().catch(err= > console.log(err));
  }

  // Assemble various middleware
  private async init() {
    const router = new Router();
    const subRouter = await autoRouter(resolve(__dirname, '/'));
    router.use(subRouter.routes(), jwt); // Add JWT authentication to the route
    this.app
      .use(cors())
      .use(loggerHandle)
      .use(errorHandle)
      .use(body({
        multipart: true
      }))
      .use(router.routes())
      .use(router.allowedMethods())
      .use(staticService(resolve(__dirname, '.. /.. /static')))
      .use(responseHandle);
  }

  start(port: number) {
    this.app.listen(port, () = > {
      connection();
      console.log('service is started'); }); }}Copy the code

Bin/WWW start the application

The main purpose of the separation of WWW is to separate the logic of the service layer from the application layer

// www
const env = process.env.NODE_ENV;

let App = null;
if (env === 'development') {
  App = require('.. /src/app/app').App;
} else {
  App = require('.. /dist/app/app').App;
}

const app = new App();

app.start(8080); // The application starts successfully, listening on port 8080
Copy the code

The tail

At this point, the entire KOA project architecture is set up. Some of you may notice that routing is not configured. This is due to the automatic routing in decorator mode implemented using the KOA-Route-Decors library. The library also implements dependency injection for the controller. This project has been uploaded to Github. Click on the link to see the source code, Huzz-Koa-Tempalte