使用 TypeScript 快速开发 Serverless REST API

本文涉及的产品
函数计算FC,每月15万CU 3个月
Serverless 应用引擎免费试用套餐包,4320000 CU,有效期3个月
简介: 这是一个对于 AWS Lambda Functions 的简单 REST API 项目,使用 TypeScript 语言编写,数据存储采用 MongoDB Atlas 云数据库,从编码到 AWS Lambda 下的单元测试,再到部署、日志调试完整的介绍了如何快速编写一个 FaaS 函数。本文你将学习到REST API with typescriptMongoDB Atlas data storageMulti-environment management under ServerlessMocha unit tests and lambda-tester interface tes

这是一个对于 AWS Lambda Functions 的简单 REST API 项目,使用 TypeScript 语言编写,数据存储采用 MongoDB Atlas 云数据库,从编码到 AWS Lambda 下的单元测试,再到部署、日志调试完整的介绍了如何快速编写一个 FaaS 函数。

本文你将学习到
REST API with typescript
MongoDB Atlas data storage
Multi-environment management under Serverless
Mocha unit tests and lambda-tester interface test
AWS lambda function log view
REST API 规划
以下是我们将要完成的 REST API 规划,包含四个 CRUD 操作

CRUD API Routes Description
POST /books 增加一本书
GET /books 获取所有书籍列表
PUT /books/:id 根据 id 更新指定编号书籍
DELETE /books/:id 根据 id 删除指定编号书籍
目录结构定义
├── app
│ ├── contrller # 控制层,解析用户输入数据,处理结果返回
│ ├── model # 数据库模型
│ ├── service # 业务逻辑层
│ └── utils # 工具类
├── config # 环境变量和配置相关
├── docs # 文档
├── tests # 单元测试
├── tsconfig.json # 指定 TypeScript 编译的参数信息
└── tslint.json # 指定 TypeScript 代码规范
├── .editorconfig # 约定编辑器的代码风格
├── .gitignore # git 提交忽略指定文件
├── .nycrc.json
├── package.json # package.json
├── serverless.yml # Serverless 配置文件
├── README.md
复制代码
Serverless 相关插件
serverless-offline
使用这个 serverless-offline 插件可以在本地启动一个 HTTP 服务器模拟 API Gateway。

安装

npm install serverless-offline -D
复制代码
添加 serverless-offline 到 serverless.yml 文件

plugins:

  • serverless-offline

复制代码
serverless-plugin-typescript 插件
零配置 TypeScript 支持的 ServerLess 插件,Github serverless-plugin-typescript

安装

npm install -D serverless-plugin-typescript typescript
复制代码
添加 serverless-plugin-typescript 到 serverless.yml 文件,确保其位于 serverless-offline 之前

plugins:

  • serverless-plugin-typescript
  • serverless-offline

复制代码
多配置环境管理
实际业务中,都会存在多套环境配置,例如:测试、预发、生产,那么在 Serverless 中如何做环境切换呢?

为云函数配置环境变量
修改 serverless.yml 文件为云函数配置环境变量,例如设置变量 NODE_ENV = dev

provider:
environment:

NODE_ENV: dev

复制代码
配置文件上传时的 incldue 和 exclude
修改 serverless.yml 文件,新增 exclude 和 incldue 配置,实现仅上传对应配置文件

exclude: 要忽略的配置文件
include: 指定的配置文件会被上传
package:
exclude:

- config/.env.stg
- config/.env.pro

include:

- config/.env.dev

复制代码
注:因为 TS 最终编译只会编译 .ts 结尾的文件,默认情况下 config 里面指定的配置文件是不会上传的

Dotenv 模块
默认情况如果我们设置了 .env 文件,dotenv 可以将此文件里设置的环境变量注入到 process.env 对象中,如果你有自己个性化定义的 .env 文件,在 dotenv 加载时指定 path 也可。

安装

npm i dotenv -S
npm i @types/dotenv-safe -D
复制代码
项目中使用

通过提取上面云函数中设置的环境变量 NODE_ENV,拼接路径 path 为 .env 指定文件路径

import dotenv from 'dotenv';
import path from 'path';

// 具体路径根据自己的项目配置来
const dotenvPath = path.join(__dirname, '../', config/.env.${process.env.NODE_ENV});
dotenv.config({
path: dotenvPath,
});
复制代码
dotenv 环境变量配置参考

github.com/motdotla/dotenv
serverlesscloud.cn/best-practice/2020-03-10-serverless-env
编码实践核心讲解
路由指定
Serverless.yml 文件中通过 handler 指定函数的访问路径,http.path 指定访问的路由,method 指定函数的请求方法。

相当于传统应用开发中,我们这样来定义 router.get('books/:id', () => { ... }) 一个路由

functions:
create:

handler: app/handler.create
events:
  - http:
      path: books
      method: post

findOne:

handler: app/handler.findOne
events:
  - http:
      path: books/{id}
      method: get

复制代码
handler 入口函数处理
入口函数,利用函数的执行上下文重用,启动环境执行代码时初始化我们的数据库链接、加载环境变量。

event、context 这些参数由 FaaS 平台提供,从 aws-lambda 中可以找到 Handler、Context 的声明,但是并没有找到关于 event 的。

// app/handler.ts
import { Handler, Context } from 'aws-lambda';
import dotenv from 'dotenv';
import path from 'path';
const dotenvPath = path.join(__dirname, '../', config/.env.${process.env.NODE_ENV});
dotenv.config({
path: dotenvPath,
});

import { books } from './model';
import { BooksController } from './contrller/books';
const booksController = new BooksController(books);

export const create: Handler = (event: any, context: Context) => {
return booksController.create(event, context);
};

export const findOne: Handler = (event: any, context: Context) => {
return booksController.findOne(event, context);
};
...
复制代码
Controller 控制器层
通过路由指定和 handler 入口函数的处理,将用户的请求基于 Path 和 Method 分发至相应 Controller 层,解析用户的输入,处理后返回。

这一层不应存在任何形式的 “SQL 查询”,如有需要它应该调用 Service 层处理业务,然后封装结果返回。

// app/controller/books.ts
...
export class BooksController extends BooksService {
constructor (books: Model) {

super(books);

}

/**

  • Create book
  • @param {*} event

*/
async create (event: any, context?: Context) {

console.log('functionName', context.functionName);
const params: CreateBookDTO = JSON.parse(event.body);

try {
  const result = await this.createBook({
    name: params.name,
    id: params.id,
  });

  return MessageUtil.success(result);
} catch (err) {
  console.error(err);

  return MessageUtil.error(err.code, err.message);
}

}

/**

  • Query book by id
  • @param event

*/
async findOne (event: any, context: Context) {

// The amount of memory allocated for the function
console.log('memoryLimitInMB: ', context.memoryLimitInMB);

const id: number = Number(event.pathParameters.id);

try {
  const result = await this.findOneBookById(id);

  return MessageUtil.success(result);
} catch (err) {
  console.error(err);

  return MessageUtil.error(err.code, err.message);
}

}
...
}
复制代码
Service 服务层
为了保证 Controller 层逻辑更加简洁,针对复杂的业务逻辑可以抽象出来做一个服务层,做到独立性、可复用性(可以被多个 Controller 层调用),这样也更有利于单元测试的编写。

// app/service/books.ts
...
export class BooksService {
private books: Model;
constructor(books: Model) {

this.books = books;

}

/**

  • Create book
  • @param params

*/
protected async createBook (params: CreateBookDTO): Promise {

try {
  const result = await this.books.create({
    name: params.name,
    id: params.id,
  });

  // Do something

  return result;
} catch (err) {
  console.error(err);

  throw err;
}

}

/**

  • Query book by id
  • @param id

*/
protected findOneBookById (id: number) {

return this.books.findOne({ id });

}
...
}
复制代码
Model 数据层
这一层链接我们的 DB,定义我们需要的 Schema,每个 Schema 都会映射到一个 MongoDB Collection 中。

// app/model/mongoose-db.ts
import mongoose from 'mongoose';

export default mongoose.connect(process.env.DB_URL, {
dbName: process.env.DB_NAME,
useUnifiedTopology: true,
useNewUrlParser: true,
});
复制代码
// app/model/books.ts
import mongoose from 'mongoose';

export type BooksDocument = mongoose.Document & {
name: string,
id: number,
description: string,
createdAt: Date,
};

const booksSchema = new mongoose.Schema({
name: String,
id: { type: Number, index: true, unique: true },
description: String,
createdAt: { type: Date, default: Date.now },
});

// Note: OverwriteModelError: Cannot overwrite Books model once compiled. error
export const books = mongoose.models.books || mongoose.model('books', booksSchema, process.env.DB_BOOKS_COLLECTION);

复制代码
单元测试
安装插件
这些插件都有什么用途,下面会介绍。

npm i @types/lambda-tester @types/chai chai @types/mocha mocha ts-node -D
复制代码
lambda-tester
以前我们可以使用 supertest 做买QQ靓号平台接口测试,但是现在我们使用 AWS Lambda 编写的 FaaS 函数则不可以这样做,例如请求中的 event、context 是与云厂商是有关联的,这里推荐一个 lambda-tester 可以实现我们需要的接口测试。

安装

npm i lambda-tester @types/lambda-tester -D
复制代码
一个简单的应用示例

在接口的路径(path)上传入参数 id

lambdaTester(findOne)
.event({ pathParameters: { id: 25768396 } })
.expectResult((result: any) => {

...

});
复制代码
sinon
例如,我们请求一个接口,接口内部依赖于 DB 获取数据,但是在做单元测试中我们如果不需要获取实际的对象,就需要使用 Stub/Mock 对我们的代码进行模拟操作。

安装

npm i sinon @types/sinon -D
复制代码
示例

以下例子中,我会做一个接口测试,通过 sinon 来模拟 mongoose 的各种方法操作。

const s = sinon
.mock(BooksModel);

s.expects('findOne')
.atLeast(1)
.atMost(3)
.resolves(booksMock.findOne);
// .rejects(booksMock.findOneError);

return lambdaTester(findOne)
.event({ pathParameters: { id: 25768396 } })
.expectResult((result: any) => {
// ...
});
复制代码
以上对 booksMock 的 findOne 做了数据返回 Mock 操作,使用 s.resolves 方法模拟了 fulfilled 成功态,如需测试 rejected 失败态需指定 s.rejects 函数。

一些常用方法

s.atLeast(1) 最少调用一次。
s.atMost(3) 最多调用三次。
s.verify() 用来验证 findOne 这个方法是否满足上面的条件。
s.restore() 使用后复原该函数,适合于对某个函数的属性进行多次 stub 操作。
测试覆盖率
单元测试用来验证代码,测试覆盖率则可以验证测试用例,这里我们选择使用 nyc。

安装

npm i nyc -D
复制代码
.nycrc.json 配置文件

{
"all": true, // 检测所有文件
"report-dir": "./coverage", // 报告文件存放位置
"extension": [".ts"], // 除了 .js 之外应尝试的扩展列表
"exclude": [ // 排除的一些文件

"coverage",
"tests"

]
}
复制代码
测试报告

下图是对本项目做的一个测试用例覆盖率报告。

图片描述
Deploy And Usage
本地部署测试
运行 npm install 安装需要的依赖
运行 npm run local 实际使用的是 serverless offline 在本地开启测试。
在 AWS 上的部署, 运行:
$ npm run deploy

or

$ serverless deploy
复制代码
期望的结果应该如下所示:

Serverless: Compiling with Typescript...
Serverless: Using local tsconfig.json
Serverless: Typescript compiled.
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service aws-node-rest-api-typescript.zip file to S3 (1.86 MB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
......................................
Serverless: Stack update finished...
Service Information
service: aws-node-rest-api-typescript
stage: dev
region: us-east-1
stack: aws-node-rest-api-typescript-dev
resources: 32
api keys:
None
endpoints:
POST - https://xxxxxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/books
PUT - https://xxxxxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/books/{id}
GET - https://xxxxxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/books
GET - https://xxxxxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/books/{id}
DELETE - https://xxxxxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/books/{id}
functions:
create: aws-node-rest-api-typescript-dev-create
update: aws-node-rest-api-typescript-dev-update
find: aws-node-rest-api-typescript-dev-find
findOne: aws-node-rest-api-typescript-dev-findOne
deleteOne: aws-node-rest-api-typescript-dev-deleteOne
layers:
None
Serverless: Removing old service artifacts from S3...
Serverless: Run the "serverless" command to setup monitoring, troubleshooting and testing.
复制代码
Usage
使用 curl 之类的工具直接向端点发送一个 HTTP 请求。

curl https://xxxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/books
复制代码
AWS Lambda 查看 Serverless 函数日志
服务上线之后难免有时会需要通过日志来排查问题,AWS 中我们可以通过管理控制台和 CLI 本地化两种方式查看。

AWS 管理控制台查看
打开 CloudWatch 控制台的日志页面。
选择您的函数 (/aws/lambda/function-name) 的日志组。
选择列表中的日志流。
AWS CLI 方式查看
安装
docs.aws.amazon.com/cli/latest/…

确认是否安装成功
which aws 或 aws --version 命令检测是否安装成功,类似以下结果,安装成功

$ which aws
/usr/local/bin/aws
$ aws --version
aws-cli/2.0.12 Python/3.7.4 Darwin/19.3.0 botocore/2.0.0dev16
复制代码
认证
安装成功,需先执行 aws configure 命令配置 aws-cli 和凭据

$ aws configure
AWS Access Key ID [None]: AKIAIOSFODNN7EXAMPLE
AWS Secret Access Key [None]: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
Default region name [None]: us-west-1
Default output format [None]:
复制代码
区域名称 region 一定要配置,如果不知道的,当时 serverless deploy 的时候也有显示,可以留意下。

终端查看

默认展示 base64 编码之后的数据

$ aws lambda invoke --function-name aws-node-rest-api-typescript-dev-find out-logger.json --log-type Tail

base64 解码日志

$ aws lambda invoke --function-name aws-node-rest-api-typescript-dev-find out-logger.json --log-type Tail --query 'LogResult' --output text | base64 -d
复制代码
Github
本示例项目,你可以在 Github 找到 Clone 下来进行学习。

仓库:github.com/Q-Angelo/aw… <-- 戳戳 Star

总结
Serverless 下的云函数开发,可以使我们更关注于业务本身,从上面示例中也可以看到我们的业务代码并没有什么区别,更多的是避免了运维、后期的扩所容等一些成本问题,还有一点不同的是入口函数,传统的应用开发我们可以通过 HTTP 的 Request、Response 做处理和响应,例如在 AWS Lambda 下我们则是通过 event、context 来处理请求和一些上下文信息。

FaaS 这一层应尽可能的轻量,更多的是业务逻辑的处理,对于数据库这种是很难做到动态化、自动伸缩,但是如果每次冷启动都去创建链接对于数据库本身也会造成压力,一方面可以选择云平台提供的,另一方面也可以自己数据库 BaaS 化,经过包装进行调用。

相关实践学习
【文生图】一键部署Stable Diffusion基于函数计算
本实验教你如何在函数计算FC上从零开始部署Stable Diffusion来进行AI绘画创作,开启AIGC盲盒。函数计算提供一定的免费额度供用户使用。本实验答疑钉钉群:29290019867
建立 Serverless 思维
本课程包括: Serverless 应用引擎的概念, 为开发者带来的实际价值, 以及让您了解常见的 Serverless 架构模式
目录
相关文章
|
4天前
|
JSON 安全 API
如何使用Python开发API接口?
在现代软件开发中,API(应用程序编程接口)用于不同软件组件之间的通信和数据交换,实现系统互操作性。Python因其简单易用和强大功能,成为开发API的热门选择。本文详细介绍了Python开发API的基础知识、优势、实现方式(如Flask和Django框架)、实战示例及注意事项,帮助读者掌握高效、安全的API开发技巧。
23 3
如何使用Python开发API接口?
|
3天前
|
缓存 监控 Java
如何运用JAVA开发API接口?
本文详细介绍了如何使用Java开发API接口,涵盖创建、实现、测试和部署接口的关键步骤。同时,讨论了接口的安全性设计和设计原则,帮助开发者构建高效、安全、易于维护的API接口。
17 4
|
3天前
|
XML JSON API
【PHP开发专栏】PHP RESTful API设计与开发
随着互联网技术的发展,前后端分离成为Web开发的主流模式。本文介绍RESTful API的基本概念、设计原则及在PHP中的实现方法。RESTful API是一种轻量级、无状态的接口设计风格,通过HTTP方法(GET、POST、PUT、DELETE)操作资源,使用JSON或XML格式传输数据。在PHP中,通过定义路由、创建控制器、处理HTTP请求和响应等步骤实现RESTful API,并强调了安全性的重要性。
10 2
|
9天前
|
前端开发 关系型数据库 API
深入浅出后端开发——从零到一构建RESTful API
本文旨在为初学者提供一个关于后端开发的全面指南,特别是如何从零开始构建一个RESTful API。我们将探讨后端开发的基本概念、所需技术栈、以及通过实际案例展示如何设计和实现一个简单的RESTful API。无论你是完全的新手还是有一定编程基础的开发者,这篇文章都将为你提供实用的知识和技巧,帮助你在后端开发的道路上迈出坚实的一步。
|
11天前
|
缓存 负载均衡 安全
后端开发的艺术:构建高效、可扩展的API
在现代软件开发中,后端开发扮演着至关重要的角色。它不仅负责处理数据存储、业务逻辑和安全性,还需要提供高效、可扩展的API供前端和其他服务使用。本文将深入探讨后端开发的关键概念和技术,帮助读者了解如何构建高效、可扩展的API,并提供一些实用的建议和最佳实践。
|
10天前
|
缓存 负载均衡 测试技术
‌API开发的基础概念和作用‌
API(Application Programming Interface)是一组定义了软件组件之间交互规则的接口。它提供了一种标准化的方式,让不同的软件组件之间可以进行通信和交互。
|
10天前
|
缓存 API 网络架构
掌握现代API开发:GraphQL vs REST
【10月更文挑战第24天】本文深入探讨了现代API开发中两种主流技术——GraphQL和REST的设计理念、技术特点及实际开发中的对比分析。GraphQL通过声明式数据请求和强类型系统提供更高的灵活性和性能,而REST则以其无状态特性和成熟的生态系统见长。文章还讨论了两者在客户端-服务器交互、安全性和工具支持方面的优劣,帮助开发者根据项目需求做出明智选择。
|
18天前
|
Java 大数据 API
别死脑筋,赶紧学起来!Java之Steam() API 常用方法使用,让开发简单起来!
分享Java Stream API的常用方法,让开发更简单。涵盖filter、map、sorted等操作,提高代码效率与可读性。关注公众号,了解更多技术内容。
|
15天前
|
存储 XML API
探索后端开发中的RESTful API设计哲学
【10月更文挑战第21天】在数字化时代,后端开发是构建强大、可靠和可扩展应用程序的基石。本文将深入探讨RESTful API的设计原则,并展示如何通过这些原则来提升API的质量和性能。我们将从基础概念出发,逐步深入到实际案例分析,揭示高效API设计的秘诀。无论你是初学者还是有经验的开发者,这篇文章都将为你提供宝贵的见解和实用的技巧,帮助你在后端开发的道路上更进一步。
|
11天前
|
关系型数据库 测试技术 API
探索后端开发:构建高效API的艺术
【10月更文挑战第25天】在数字化时代,后端开发不仅仅是编写代码那么简单。它是连接用户与数据的桥梁,是实现业务逻辑的基石。本文将深入探讨如何构建高效的API,从理解RESTful原则到选择合适的框架,再到处理数据库交互,每一步骤都是精心策划的舞蹈。我们将通过实际案例,揭示如何在保证性能和安全性的同时,提供流畅的用户体验。让我们一起走进后端开发的世界,发现那些隐藏在代码背后的智慧和创造力。

热门文章

最新文章

相关产品

  • 函数计算
  • 下一篇
    无影云桌面