使用typescript&koa&typeorm开发后端api服务器

查看完整项目 请移步至 github

joi, swagger, koa 通过 decorator 的方式连接起来, 这是此项目的开发初衷.

[x] koa生态系统
[x] jwt登录验证
[x] 自动生成swagger接口文档
[x] nodemon 自动重启
[x] 使用 decorator 的方式完成接口参数验证
[x] controller 错误自动捕捉
[x] typescript 支持
[x] 基于 typeorm 的数据库 orm 支持
[x] mysql 事务支持(隔离支持)
[ ] 根据typeorm实体自动生成swagger的definition

开始使用

定义 controller

应用中所有请求均为一个 js class, 加上 @controller 即可

import {
  controller,
} from "../decorators";

@controller('/example')
export default class ExampleController {
   /* whatever you like */
}

上述例子即可实现一个基础 controller 的定义, @controller 中参数即为此控制器对应的响应路径前缀.

当然要完成一个简单请求, 这是不够的, 因为还没有定义与之相关的处理方法

定义处理函数

应用包装了 @get, @post, @put, @delete, @update 等常用http方法, 我们只需要为上一步中 controllermethod 上面加上对应方法的装饰器即可.

import {
  get,
  controller,
} from "../decorators";

@controller('/example')
export default class ExampleController {
  @get('/hello')
  async Hello() {
    return 'hello world'
  }
}

现在, 切换到项目根目录, 执行 npm start, 打开浏览器在地址栏输入 http://localhost:3002/example/hello 即可.

处理响应

通过包装 koa 的响应方法, 现在我们只需要在 controller 的方法下面直接 return 需要响应的值即可, 装饰器会接收响应参数并返回到浏览器.

需要注意: 这意味着我们必须显示return一个值给装饰器

如以下行为是不被建议的:

import {
  get,
  controller,
} from "../decorators";

export default class ExampleController {
  @get('/hello')
  async Hello() {
     // bad 没有显示的return
  }

  @get('/hello')
  async Hello() {
     // bad 没有return任何有价值的东西
     return ;
  }

  @get('/hello')
  async Hello() {
     // bad 同上, 没有return任何有价值的东西
     return undefined;
  }
  
  @get('/hello')
  async Hello(ctx) {
     // bad 请不要直接使用ctx.body = anything; 这会被覆盖
     ctx.body = 'hello';
     return 'world';
  }

  @get('/hello')
  async Hello(ctx) {
     // bad 这样实际是可以运行的, 但是仍然不推荐使用
     ctx.body = 'hello';
  }

  @get('/hello')
  async Hello() {
     // good! nice!
     return = 'hello';
  }
}

处理请求

api 系统中, 参数不可避免, 而且在处理方法内部对参数进行校验这实际会写上很多的样板代码, 也影响业务逻辑.
因此, 我们采用 joi 进行参数验证.

import {
  get,
  parameter,
  controller,
} from "../decorators";
import * as joi from 'joi';
import { IContext } from "../decorators/interface";

@controller('/example')
export default class ExampleController {
  @get('/hello')
  @parameter('params', joi.object().keys({
    a: joi.number().required(),
    b: joi.string()
  }), ENUM_PARAM_IN.body)
  @parameter('userId', joi.number().integer().description('用户id').required(), ENUM_PARAM_IN.path)
  @parameter('param1', joi.string().description('其他参数').required(), ENUM_PARAM_IN.query)
  async Hello(ctx: IContext) {
       // IContext 为定义的一个ts类型, 扩展了 koa 的 ctx
       // 为ctx加上了 $getParams 方法, 方便获取验证成功后的请求参数
       return ctx.$getParams();
  }
}

如上: 通过 @parameter 装饰器我们可以很方便定义接口参数, 并借助 joi 的魔力对其进行验证
系统可以对 querystring, path, body 进行参数验证
ENUM_PARAM_IN 为系统定义的 enum, 包含 'path' | 'body' | 'query', 默认 query

处理错误

每个控制器均支持错误 Error Catch 的能力

我们可以直接在应用中使用 throw 抛出错误, 应用在方法外层会自动捕捉并返回给前端, 参考以下示例

import {
  get,
  controller,
} from "../decorators";
import { IContext } from "../decorators/interface";

@controller('/example')
export default class ExampleController {
  @get('/hello')
  async Hello() {
     // 抛出一个默认的 500 错误, error message会默认发送给前端
     throw new Error('just an error');
  }
  @get('/hello')
  async Hello(ctx: IContext) {
     // 通过ctx.throw 我们可以抛出一个自定义状态码的错误
     ctx.throw(400, 'just an error');
  }
}

生成 swagger

应用提供了 @description, @tag, @summary, @response 等装饰器来处理swagger的情况

import {
  post,
  tag,
  summary,
  response,
  controller,
} from "../decorators";
import * as omit from 'omit.js';
import * as joi from 'joi';
import * as jwt from "jsonwebtoken";
import { User } from '../entity/User';
import { AppConfig } from '../utils/config';
import UserSchema from "../definitions/User";
import { Like } from "typeorm";
import { IContext } from "../decorators/interface";

@controller('/example')
export default class ExampleController {
  @post('/login')
  @parameter(
    'body', 
    joi.object().keys({
      name: joi.string().required().description('用户名'),
      password: joi.string().required().description('密码'),
    }), ENUM_PARAM_IN.body
  )
  @description('用户登录例子')
  @tag('用户管理')
  @summary('用户登录')
  @response(200, {
    user: { $ref: UserSchema, desc: '用户信息' },
    token: joi.string().description('token, 需要每次在请求头或者cookie中带上'),
  })
  async login(ctx: IContext) {
    const { name, password }: User = ctx.$getParams();
    const user: User = await User.findOne({ name });
    if (!user || user.password !== password) {
      throw new Error('用户名密码不匹配');
    }
    const token = jwt.sign({
      data: user.id
    }, AppConfig.appKey, { expiresIn: 60 * 60 });
    ctx.cookies.set('token', token);
    return {
      token,
      user: omit(user, ['password'])
    };
  }
}

现在, 打开浏览器, 在地址栏输入 http://localhost:3002/docs, 即可查看生成的接口swagger文档

user: { $ref: UserSchema, desc: '用户信息' }, 这里面的 $ref, 即转换后的 swagger definition, 在./src/definitions目录下即可定义

使用数据库

应用使用 typeorm 来作为数据库的 orm 工具.

  1. 安装并登录 mysql 创建一个数据库
sudo apt install mysql-server mysql-client

mysql -u root -p

> create database test default charset=utf8;
  1. 编辑 ormconfig.js 文件, 修改数据库相关配置

  2. ./src/entity 目录下面定义 typeorm 实体, 并定义实体的相关属性, 详细配置可参考 typeorm文档

  3. controller 中导入上一步中定义的模型, 使用方式参考 ./src/controlls/user.ts


使用事务

我们在 ctx 中内置了 typeorm 的 manager, 在控制器开始前开启一个 typeorm 的事务, 检测到应用内抛出的异常之后, 则自动回滚事务, 若应用正常被处理, 则自动提交事务.

注意: 因为需要检测应用内异常, 所以只能通过throw 方式抛出的异常才能被正确处理, 而不能使用ctx.throw

一个栗子:

  /**
   * 新增用户
   */
  @post('/add')
  @tag('用户管理')
  @parameter(
    'body', 
    joi.object().keys({
      name: joi.string().required().description('用户名'),
      password: joi.string().required().description('密码'),
    }), ENUM_PARAM_IN.body
  )
  @summary('添加管理员')
  @login_required()
  @response(200, { $ref: UserSchema })
  async addUser(ctx: IContext) {
    const userInfo: User = ctx.$getParams();
    const lastUser = await User.findOne({ name: userInfo.name });
    if (lastUser) {
      throw new Error('用户已存在');
    }
    const user = new User(userInfo);
    await ctx.manager.save(user);
    return omit(user, ['password']);
  }

登录验证

系统提供了 login_required 装饰器, 使用时加上即可, 栗子见上面

目录说明

.
├── ./ormconfig.js  // typeorm配置文件
├── ./package.json 
├── ./package-lock.json
├── ./readme.md // readme
├── ./src // 源码目录
│   ├── ./upload // 文件上传目录
│   ├── ./src/controllers // 控制器相关
│   │   ├── ./src/controllers/example.ts 
│   ├── ./src/decorators // decorators相关
│   │   ├── ./src/decorators/controller.ts
│   │   ├── ./src/decorators/definition.ts
│   │   ├── ./src/decorators/description.ts
│   │   ├── ./src/decorators/index.ts
│   │   ├── ./src/decorators/interface.ts
│   │   ├── ./src/decorators/ischema.ts
│   │   ├── ./src/decorators/login_required.ts
│   │   ├── ./src/decorators/method.ts
│   │   ├── ./src/decorators/parameter.ts
│   │   ├── ./src/decorators/response.ts
│   │   ├── ./src/decorators/summary.ts
│   │   ├── ./src/decorators/tag.ts
│   │   └── ./src/decorators/utils.ts
│   ├── ./src/definitions // 主要用于swagger中模型
│   │   ├── ./src/definitions/BaseSchema.ts
│   │   └── ./src/definitions/User.ts
│   ├── ./src/entity // 数据库表(typeorm实体)
│   │   ├── ./src/entity/BaseEntity.ts
│   │   └── ./src/entity/User.ts
│   ├── ./src/main.ts // 应用入口
│   └── ./src/utils // 工具目录
│       ├── ./src/utils/config.ts // 应用配置
│       ├── ./src/utils/JoiToSwagger.ts 
│       └── ./src/utils/middlewares.ts // koa middleware相关
├── ./tsconfig.json // ts配置
├── ./tslint.json
├── ./typings.json
└── ./yarn.lock

持续更新中, 更多功能请关注此仓库...

你可能感兴趣的:(使用typescript&koa&typeorm开发后端api服务器)