
掌握API建模:基本概念和實(shí)踐
為了在 Node.js 中 構(gòu)建 API,我們將使用 Nest.js。它是一個(gè)相當(dāng)靈活的框架,建立在 Express.js 的基礎(chǔ)上,可以讓你在短時(shí)間內(nèi)制作出 Node.js 服務(wù),因?yàn)樗闪撕芏嗪霉δ埽ㄈ缤耆念愋突С帧⒁蕾囎⑷搿⒛K管理和更多)。
為了更快地開始工作,Nest.js 附帶了一個(gè)很好的 CLI 工具,可以為我們創(chuàng)建項(xiàng)目模板。我們開始用以下幾行代碼生成我們的項(xiàng)目:
npm i -g @nestjs/cli
nest new project-name
更多的 Nest.js 和它的 CLI
讓我們測(cè)試一下,看看到目前為止是否一切正常:
npm run start:dev
我們將使用 TypeORM 來管理我們的數(shù)據(jù)庫(kù)架構(gòu)。TypeORM 的優(yōu)點(diǎn)是:它可以讓你通過代碼來描述數(shù)據(jù)實(shí)體模型,然后能夠應(yīng)用和同步這些模型到表結(jié)構(gòu)的數(shù)據(jù)庫(kù)。(這不僅適用于 PostgreSQL 數(shù)據(jù)庫(kù),還適用于其他數(shù)據(jù)庫(kù),可以在 TypeORM 文檔中找到支持哪些數(shù)據(jù)庫(kù))
使用 docker 自動(dòng)化設(shè)置本地 PostgreSQL 數(shù)據(jù)庫(kù)實(shí)例。
要在本地實(shí)現(xiàn)數(shù)據(jù)持久性,我們現(xiàn)在需要一個(gè)數(shù)據(jù)庫(kù)服務(wù)器和一個(gè)要連接的數(shù)據(jù)庫(kù)。一種方法是在本地機(jī)器上設(shè)置一個(gè) PostgreSQL 數(shù)據(jù)庫(kù)服務(wù)器,但這樣做不是很好。因?yàn)檫@樣項(xiàng)目與我們的本地?cái)?shù)據(jù)庫(kù)服務(wù)器會(huì)過于耦合。這意味著如果你和一個(gè)團(tuán)隊(duì)一起做一個(gè)項(xiàng)目,只要切換機(jī)器就要在每臺(tái)機(jī)器上設(shè)置數(shù)據(jù)庫(kù)服務(wù)器,或者以某種方式編寫安裝指南等(當(dāng)你團(tuán)隊(duì)的開發(fā)同學(xué)有不同的操作系統(tǒng)時(shí),事情變得更加棘手)。
那么我們?nèi)绾慰朔@一點(diǎn)呢?讓這個(gè)步驟自動(dòng)化!
我們使用預(yù)構(gòu)建的 PostgreSQL docker 鏡像并將數(shù)據(jù)庫(kù)服務(wù)器作為 docker 進(jìn)程運(yùn)行。我們可以用幾行 shell 代碼編寫一個(gè)完整的設(shè)置來讓我們的服務(wù)器實(shí)例運(yùn)行并準(zhǔn)備一個(gè)空的數(shù)據(jù)庫(kù)準(zhǔn)備連接。因?yàn)樗强蓮?fù)用的,并且設(shè)置代碼可以與項(xiàng)目代碼的其余部分一起在源代碼管理中進(jìn)行管理,這使得團(tuán)隊(duì)中其他開發(fā)人員的 “入門” 變得非常簡(jiǎn)單。
下面是這個(gè)腳本的樣子:
#!/bin/bash
set -e
SERVER="my_database_server";
PW="mysecretpassword";
DB="my_database";
echo "echo stop & remove old docker [$SERVER] and starting new fresh instance of [$SERVER]"
(docker kill $SERVER || :) && \
(docker rm $SERVER || :) && \
docker run --name $SERVER -e POSTGRES_PASSWORD=$PW \
-e PGPASSWORD=$PW \
-p 5432:5432 \
-d postgres
# wait for pg to start
echo "sleep wait for pg-server [$SERVER] to start";
SLEEP 3;
# create the db
echo "CREATE DATABASE $DB ENCODING 'UTF-8';" | docker exec -i $SERVER psql -U postgres
echo "\l" | docker exec -i $SERVER psql -U postgres
讓我們將該命令添加到我們的 package.json 運(yùn)行腳本中,以便我們可以輕松執(zhí)行它。
"start:dev:db": "./src/scripts/start-db.sh"
現(xiàn)在我們有了一個(gè)可以運(yùn)行的命令,它會(huì)設(shè)置數(shù)據(jù)庫(kù)服務(wù)器和一個(gè)普通的數(shù)據(jù)庫(kù)。
為了使過程更健壯,我們將為 docker 容器使用相同的名稱(腳本中的 $SERVER var),并添加一個(gè)額外的檢查:如果有同名的容器正在運(yùn)行,那么將結(jié)束并刪除它以確保干凈狀態(tài)。
就像所有事情一樣,已經(jīng)有一個(gè) NPM 模塊可以幫助您將 Nest.js 項(xiàng)目掛鉤到您的數(shù)據(jù)庫(kù)。讓我們使用預(yù)構(gòu)建的 NestJS-to-TypeORM 模塊為我們的項(xiàng)目添加 TypeORM 支持。
您可以像這樣添加所需的模塊:
npm install --save @nestjs/typeorm typeorm pg
我們可以在 Nest.js 中配置 TypeORM 連接到哪個(gè)數(shù)據(jù)庫(kù)服務(wù)器,方法是使用 TypeOrmModule。它有一個(gè) forRoot
方法,我們可以傳入配置。我們知道配置在本地開發(fā)和生產(chǎn)環(huán)境中會(huì)有所不同。所以,這個(gè)過程在某種程度上必須是通用的,以便它可以在不同運(yùn)行環(huán)境提供不同的配置。我們可以編寫以下配置服務(wù)。這個(gè)配置類的功能是在我們的 API Server main.ts 啟動(dòng)之前運(yùn)行。它可以從環(huán)境變量中讀取配置,然后在運(yùn)行時(shí)以只讀方式提供值。為了使 dev 和 prod 靈活,我們將使用 dotenv 模塊。
npm install --save dotenv
有了這個(gè)模塊,我們可以在本地開發(fā)的項(xiàng)目根目錄中有一個(gè) “.env” 文件來準(zhǔn)備配置值,而在生產(chǎn)中,我們可以從生產(chǎn)服務(wù)器上的環(huán)境變量中讀取值。這是一種非常靈活的方法,還允許您使用一個(gè)文件輕松地與團(tuán)隊(duì)中的其他開發(fā)人員共享配置。注意:我強(qiáng)烈建議 git 忽略此文件,因?yàn)槟阌锌赡軙?huì)將生產(chǎn)環(huán)境的賬號(hào)密碼放入此文件中,所以你不應(yīng)把配置文件提交到項(xiàng)目中而造成意外泄露。
這是您的 .env 文件的樣子:
POSTGRES_HOST=127.0.0.1
POSTGRES_PORT=5432
POSTGRES_USER=postgres
POSTGRES_PASSWORD=mysecretpassword
POSTGRES_DATABASE=my_database
PORT=3000
MODE=DEV
RUN_MIGRATIONS=true
因此,我們的 ConfigService 將作為單例服務(wù)運(yùn)行,在啟動(dòng)時(shí)加載配置值并將它們提供給其他模塊。我們將在服務(wù)中包含一個(gè)容錯(cuò)模式。這意味著如果獲取一個(gè)不存在的值,它將拋出含義完整的錯(cuò)誤。這使您的設(shè)置更加健壯,因?yàn)槟鷮⒃跇?gòu)建 / 啟動(dòng)時(shí)檢測(cè)配置錯(cuò)誤,而不是在運(yùn)行時(shí)生命周期。這樣您將能夠在部署 / 啟動(dòng)服務(wù)器時(shí)盡早地檢測(cè)到這一點(diǎn),而不是在消費(fèi)者使用您的 api 時(shí)才發(fā)現(xiàn)問題。
這是您的 ConfigService 的外觀以及我們將其添加到 Nest.js 應(yīng)用程序模塊的方式:
// app.module.ts
import { Module } from'@nestjs/common';
import { TypeOrmModule } from'@nestjs/typeorm';
import { AppController } from'./app.controller';
import { AppService } from'./app.service';
import { configService } from'./config/config.service';
@Module({
imports: [
TypeOrmModule.forRoot(configService.getTypeOrmConfig())
],
controllers: [AppController],
providers: [AppService],
})
exportclass AppModule { }
// src/config/config.service.ts
import { TypeOrmModuleOptions } from'@nestjs/typeorm';
require('dotenv').config();
class ConfigService {
constructor(private env: { [k: string]: string | undefined }) { }
private getValue(key: string, throwOnMissing = true): string {
const value = this.env[key];
if (!value && throwOnMissing) {
thrownewError(config error - missing env.${key}
);
}
return value;
}
publicensureValues(keys: string[]) {
keys.forEach(k =>this.getValue(k, true));
returnthis;
}
publicgetPort() {
returnthis.getValue('PORT', true);
}
publicisProduction() {
const mode = this.getValue('MODE', false);
return mode != 'DEV';
}
public getTypeOrmConfig(): TypeOrmModuleOptions {
return {
type: 'postgres',
host: this.getValue('POSTGRES_HOST'),
port: parseInt(this.getValue('POSTGRES_PORT')),
username: this.getValue('POSTGRES_USER'),
password: this.getValue('POSTGRES_PASSWORD'),
database: this.getValue('POSTGRES_DATABASE'),
entities: ['**/*.entity{.ts,.js}'],
migrationsTableName: 'migration',
migrations: ['src/migration/*.ts'],
cli: {
migrationsDir: 'src/migration',
},
ssl: this.isProduction(),
};
}
}
const configService = new ConfigService(process.env)
.ensureValues([
'POSTGRES_HOST',
'POSTGRES_PORT',
'POSTGRES_USER',
'POSTGRES_PASSWORD',
'POSTGRES_DATABASE'
]);
export { configService };
npm i --save-dev nodemon ts-node
然后在 root 中添加一個(gè)帶有調(diào)試和 ts-node 支持的 nodemon.json 文件
{
"watch": ["src"],
"ext": "ts",
"ignore": ["src/**/*.spec.ts"],
"exec": "node --inspect=127.0\. 0.1:9223 -r ts-node/register -- src/main.ts",
"env": {}
}
最后我們將 package.json 中的 start:dev 腳本更改為:
"start:dev": "nodemon --config nodemon.json",
這樣可以通過 npm run start:dev
來啟動(dòng)我們的 API-server,在啟動(dòng)時(shí)它應(yīng)該從 ConfigService 中獲取 .env 對(duì)應(yīng)環(huán)境的 values,然后將 typeORM 連接到我們的數(shù)據(jù)庫(kù),而且它不綁定在我的機(jī)器上。
TypeORM 支持自動(dòng)加載數(shù)據(jù)模型實(shí)體。您可以簡(jiǎn)單地將它們?nèi)糠旁谝粋€(gè)文件夾中,并在您的配置中使用一種模式加載它們 —— 我們將我們的放在 model/.entity.ts 中。(見實(shí)體的 TypeOrmModuleOptions 中的 ConfigService)
TypeORM 的另一個(gè)特性是這些實(shí)體模型支持繼承。
例如,如果您希望每個(gè)實(shí)體都擁有某些數(shù)據(jù)字段。
例如:自動(dòng)生成的 uuid id 字段 和 createDateTime 字段,lastChangedDateTime 字段。
注意:這些基類應(yīng)該是 abstract。
因此,在 TypeORM 中定義數(shù)據(jù)模型實(shí)體將如下所示:
// base.entity.ts
import { PrimaryGeneratedColumn, Column, UpdateDateColumn, CreateDateColumn } from'typeorm';
exportabstractclass BaseEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'boolean', default: true })
isActive: boolean;
@Column({ type: 'boolean', default: false })
isArchived: boolean;
@CreateDateColumn({ type: 'timestamptz', default: () =>'CURRENT_TIMESTAMP' })
createDateTime: Date;
@Column({ type: 'varchar', length: 300 })
createdBy: string;
@UpdateDateColumn({ type: 'timestamptz', default: () =>'CURRENT_TIMESTAMP' })
lastChangedDateTime: Date;
@Column({ type: 'varchar', length: 300 })
lastChangedBy: string;
@Column({ type: 'varchar', length: 300, nullable: true })
internalComment: string | null;
}
// item.entity.ts
import { Entity, Column } from'typeorm';
import { BaseEntity } from'./base.entity';
@Entity({ name: 'item' })
exportclass Item extends BaseEntity {
@Column({ type: 'varchar', length: 300 })
name: string;
@Column({ type: 'varchar', length: 300 })
description: string;
}
在 typeORM 文檔中查找更多支持的數(shù)據(jù)注釋。
讓我們啟動(dòng)我們的 API,看看它是否有效。
npm run start:dev:db
npm run start:dev
實(shí)際上我們的數(shù)據(jù)庫(kù)并沒有立即反映我們的數(shù)據(jù)模型,TypeORM 能夠?qū)⒛臄?shù)據(jù)模型同步到數(shù)據(jù)庫(kù)中的表中。數(shù)據(jù)模型自動(dòng)同步很好,但也很危險(xiǎn)。為什么?在前期開發(fā)中,您可能沒有把所有數(shù)據(jù)實(shí)體都整理清楚。因此,您在代碼中更改了實(shí)體類, typeORM 會(huì)為你自動(dòng)同步字段, 但是,一旦您的數(shù)據(jù)庫(kù)中有實(shí)際數(shù)據(jù),后期打算修改字段類型或其他操作時(shí),TypeORM 將通過刪除并重新創(chuàng)建數(shù)據(jù)庫(kù)表來更改數(shù)據(jù)庫(kù),這意味著你極有可能丟失了表內(nèi)的數(shù)據(jù)。當(dāng)然在生產(chǎn)環(huán)境中你應(yīng)該避免這種意想不到情況發(fā)生。
這就是為什么我更喜歡從一開始就直接在代碼中處理數(shù)據(jù)庫(kù)遷移。
這也將幫助您和您的團(tuán)隊(duì)更好地跟蹤和理解數(shù)據(jù)結(jié)構(gòu)的變化,并迫使您更積極地思考這一點(diǎn):怎樣做可以幫助您避免生產(chǎn)環(huán)境中的破壞性更改和數(shù)據(jù)丟失。
幸運(yùn)的是 TypeORM 提供了一個(gè)解決方案和 CLI
命令,它為你處理生成 SQL 命令的任務(wù)。然后,您可以輕松驗(yàn)證和測(cè)試這些,而無需在后臺(tái)使用任何黑魔法。
以下是如何設(shè)置 typeORM CLI 的最佳實(shí)踐。
我們已經(jīng)在 ConfigService 中添加了所有必要的配置,但是 typeORM CLI 與 ormconfig.json 是同時(shí)生效的,所以我們希望與正式環(huán)境的 CLI 區(qū)分開來。添加一個(gè)腳本來編寫配置 json 文件并將其添加到我們的.gitignore -list:
import fs = require('fs');
fs.writeFileSync('ormconfig.json', JSON.stringify(configService.getTypeOrmConfig(), null, 2)
);
添加一個(gè) npm 腳本任務(wù)來運(yùn)行它以及 typeorm:migration:generate 和 typeorm:migration:run 的命令。
像這樣 ormconfig 將在運(yùn)行 typeORM CLI 命令之前生成。
"pretypeorm": "(rm ormconfig.json || :) && ts-node -r tsconfig-paths/register src/scripts/write-type-orm-config.ts",
"typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js",
"typeorm:migration:generate": "npm run typeorm -- migration:generate -n",
"typeorm:migration:run": "npm run typeorm -- migration:run"
現(xiàn)在我們可以運(yùn)行這個(gè)命令來創(chuàng)建一個(gè)初始化遷移:
npm run typeorm:migration:generate -- my_init
這會(huì)將 typeORM 連接到您的數(shù)據(jù)庫(kù)并生成一個(gè)數(shù)據(jù)庫(kù)遷移腳本 my_init.ts(在 typescript 中)并將其放入您項(xiàng)目的遷移文件夾中。
注意:您應(yīng)該將這些遷移腳本提交到您的源代碼管理中,并將這些文件視為只讀。
如果你想改變一些東西,想法是使用 CLI 命令在頂部添加另一個(gè)遷移。
npm run typeorm:migration:run
現(xiàn)在我們擁有了創(chuàng)建和運(yùn)行遷移所需的所有工具,而無需運(yùn)行 API 服務(wù)器項(xiàng)目,它在開發(fā)時(shí)為我們提供了很大的靈活性,我們可以隨時(shí)重新運(yùn)行、重新創(chuàng)建和添加它們。然而,在生產(chǎn)或階段環(huán)境中,您實(shí)際上經(jīng)常希望在部署之后 / 之后啟動(dòng) API 服務(wù)器之前自動(dòng)運(yùn)行遷移腳本。
為此,您只需添加一個(gè) start.sh 腳本即可。
您還可以添加一個(gè)環(huán)境變量 RUN_MIGRATIONS=<0|1> 來控制遷移是否應(yīng)該自動(dòng)運(yùn)行。
#!/bin/bash
設(shè)置 -e
設(shè)置 -x
如果 [ "$RUN_MIGRATIONS" ]; 然后
回顯“正在運(yùn)行的遷移”;
npm run typeorm:migration:run
fi
回聲“啟動(dòng)服務(wù)器”;
npm run start:prod
我們通過 API 完成同步數(shù)據(jù)庫(kù)字段工作 – 但我們的數(shù)據(jù)庫(kù)實(shí)際上反映了我們的數(shù)據(jù)模型嗎?
可以通過對(duì) DB 運(yùn)行一些 CLI 腳本查詢或使用 UI 數(shù)據(jù)庫(kù)管理工具進(jìn)行快速調(diào)試來檢查這一點(diǎn)。
使用 PostgreSQL 數(shù)據(jù)庫(kù)時(shí),我使用 pgAdmin。
這是一個(gè)非常強(qiáng)大的工具,有一個(gè)漂亮的用戶界面。但是,我建議您使用以下工作流程:
我們現(xiàn)在可以看到表在數(shù)據(jù)庫(kù)中創(chuàng)建。1. 我們?cè)陧?xiàng)目中定義的項(xiàng)目表。2. 一個(gè)遷移表,在這個(gè)表中 typeORM 跟蹤已經(jīng)在這個(gè)數(shù)據(jù)庫(kù)上執(zhí)行了哪個(gè)遷移。(注意:您也應(yīng)該將此表視為只讀,否則 typeORM CLI 會(huì)混淆)
添加一些業(yè)務(wù)邏輯
現(xiàn)在讓我們添加一些業(yè)務(wù)邏輯。
為了演示,我將添加一個(gè)簡(jiǎn)單的 endpoint,它將返回表中的數(shù)據(jù)。
我們使用 Nest.js CLI 添加一個(gè)項(xiàng)目控制器和一個(gè)項(xiàng)目服務(wù)。
nest -- generate controller item
nest -- generate service item
這將為我們生成一些模板,然后我們添加:
// item.service.ts
import { Injectable } from'@nestjs/common';
import { InjectRepository } from'@nestjs/typeorm';
import { Item } from'../model/item.entity';
import { Repository } from'typeorm';
@Injectable()
exportclass ItemService {
constructor(@InjectRepository(Item) private readonly repo: Repository<Item>) { }
publicasyncgetAll() {
returnawaitthis.repo.find();
}
}
// item.controller.ts
import { Controller, Get } from'@nestjs/common';
import { ItemService } from'./item.service';
@Controller('item')
exportclass ItemController {
constructor(private serv: ItemService) { }
@Get()
publicasyncgetAll() {
returnawaitthis.serv.getAll();
}
}
然后通過 ItemModule 連接在一起,然后在 AppModule 中導(dǎo)入。
// item.module.ts
import { Module } from'@nestjs/common';
import { TypeOrmModule } from'@nestjs/typeorm';
import { ItemService } from'./item.service';
import { ItemController } from'./item.controller';
import { Item } from'../model/item.entity';
@Module({
imports: [TypeOrmModule.forFeature([Item])],
providers: [ItemService],
controllers: [ItemController],
exports: []
})
exportclass ItemModule { }
啟動(dòng) API 后,curl 試試:
curl localhost:3000/item | jq
[] # << indicating no items in the DB - cool :)
不要通過您的 API 向消費(fèi)者公開您在持久性上的實(shí)際數(shù)據(jù)模型。
當(dāng)你用一個(gè)數(shù)據(jù)傳輸對(duì)象包裝每個(gè)數(shù)據(jù)實(shí)體時(shí),你必須對(duì)它做序列化和反序列化。
在內(nèi)部數(shù)據(jù)模型(API 到數(shù)據(jù)庫(kù))和外部模型(API 消費(fèi)者到 API)之間應(yīng)該是有區(qū)別的。從長(zhǎng)遠(yuǎn)來看,這將幫助您解耦,令維護(hù)變得更容易。
@nestjs/swagger
、class-validator
和 class-transformer
。// item.dto.ts
import { ApiModelProperty } from'@nestjs/swagger';
import { IsString, IsUUID, } from'class-validator';
import { Item } from'../model/item.entity';
import { User } from'../user.decorator';
exportclass ItemDTO implements Readonly<ItemDTO> {
@ApiModelProperty({ required: true })
@IsUUID()
id: string;
@ApiModelProperty({ required: true })
@IsString()
name: string;
@ApiModelProperty({ required: true })
@IsString()
description: string;
publicstaticfrom(dto: Partial<ItemDTO>) {
const it = new ItemDTO();
it.id = dto.id;
it.name = dto.name;
it.description = dto.description;
return it;
}
publicstaticfromEntity(entity: Item) {
returnthis.from({
id: entity.id,
name: entity.name,
description: entity.description
});
}
publictoEntity(user: User = null) {
const it = new Item();
it.id = this.id;
it.name = this.name;
it.description = this.description;
it.createDateTime = newDate();
it.createdBy = user ? user.id : null;
it.lastChangedBy = user ? user.id : null;
return it;
}
}
現(xiàn)在我們可以像這樣簡(jiǎn)單地使用 DTO:
// item.controller.ts
@Get()
publicasync getAll(): Promise<ItemDTO[]> {
returnawaitthis.serv.getAll()
}
@Post()
publicasync post(@User() user: User, @Body() dto: ItemDTO): Promise<ItemDTO> {
returnthis.serv.create(dto, user);
}
// item.service.ts
publicasync getAll(): Promise<ItemDTO[]> {
returnawaitthis.repo.find()
.then(items => items.map(e => ItemDTO.fromEntity(e)));
}
publicasync create(dto: ItemDTO, user: User): Promise<ItemDTO> {
returnthis.repo.save(dto.toEntity(user))
.then(e => ItemDTO.fromEntity(e));
}
設(shè)置 OpenAPI(Swagger)
DTO 方法還使您能夠從它們生成 API 文檔(openAPI aka swagger docs)。您只需安裝:
npm install --save @nestjs/swagger swagger-ui-express
并在 main.ts 中添加這幾行
// main.ts
asyncfunction bootstrap() {
const app = await NestFactory.create(AppModule);
if (!configService.isProduction()) {
constdocument = SwaggerModule.createDocument(app, new DocumentBuilder()
.setTitle('Item API')
.setDescription('My Item API')
.build());
SwaggerModule.setup('docs', app, document);
}
await app.listen(3000);
}
本文章轉(zhuǎn)載微信公眾號(hào)@騰訊IMWeb前端團(tuán)隊(duì)
掌握API建模:基本概念和實(shí)踐
程序員常用的API接口管理工具有哪些?
簡(jiǎn)化API縮寫:應(yīng)用程序編程接口終極指南
如何為你的項(xiàng)目挑選最佳API?完整選擇流程解讀
應(yīng)用程序開發(fā)蓬勃發(fā)展的必備開放API
.NET Core Web APi類庫(kù)如何內(nèi)嵌運(yùn)行和.NET Core Web API 中的異常處理
.NET Core Web API + Vue By Linux and Windows 部署方案知識(shí)點(diǎn)總結(jié)
優(yōu)化利潤(rùn):計(jì)算并報(bào)告OpenAI支持的API的COGS
用于集成大型語言模型的LLM API
對(duì)比大模型API的內(nèi)容創(chuàng)意新穎性、情感共鳴力、商業(yè)轉(zhuǎn)化潛力
一鍵對(duì)比試用API 限時(shí)免費(fèi)