原文:How to build a basic API with TypeScript, Koa, and TypeORM
本教程探讨如何使用 TypeScript、Koa 和 TypeORM 构建基本 API。 你需要一个支持 await/async 的 Node.js 版本。
TypeScript 是一种开源编程语言,在软件开发社区中越来越流行。
由 Microsoft 开发和维护,它减少了像我这样的软件工程师需要编写的测试数量,并通过在您键入时报告错误来加快开发速度,这要归功于快速和智能的 VSCode 集成。
本教程将通过演示实现和使用 TypeScript 的简单程度,向您展示 TypeScript 的一些优势。 我们还将探索附加功能(例如类装饰器)如何进一步加速您的开发。
为此,我们将创建一个简单的 API,用于存储电影名称、发行年份和数字评级。 然后,数据将使用 TypeORM(一种 TypeScript 友好的数据映射器)存储在 PostgreSQL 中。
这篇文章基于 Node.js 10,但 8 就可以了。 您还需要一个可用的 PostgreSQL 安装。 我们将记录如何通过 Docker 启动和运行它,但如果您没有可用的 Docker,您可以尝试在线服务,例如 ElephantSQL。
设置项目
Node 基础
首先,我们将创建一个基本的 Node.js 项目。 使用以下命令开始:
mkdir -p typescript-koa && cd typescript-koa
然后我们要创建 Node.js 项目。 我们可以使用速记,因为我们不会创建一个实时项目:
npm init -y
最后,我们将要获取正常的 Node 依赖项:
现在我们已准备好设置 TypeScript。
TypeScript 设置
我们已经安装了基本的 Node 依赖项,理论上我们现在就可以开始了。 但是我们想要 TypeScript,所以让我们配置它。 我们首先需要一些额外的依赖。 不过,这些只是这次的开发依赖项:
npm i -D typescript ts-node tslint tslint-config-airbnb nodemon
这将为我们提供启动和运行所需的大部分环境。 通常,我会建议不要使用 ts-node 并提倡使用 Docker,但这只是一个简短的概述,我们现在将使用它。
最后,我们将要添加我们的类型定义。 以前,我们需要为此使用 Typings,但现在我们可以从 @types 组织安装。 如果您想了解更多信息,您绝对应该查看绝对类型的 GitHub 存储库和来自 Microsoft 的 TypeSearch。
npm i -D [@types](/user/types)/{node,koa,koa-router,http-status-codes,koa-bodyparser}
安装@types/node 将安装最新版本的类型定义。 如果您运行的是 NodeJS 8,那么您需要在安装时指定您的特定版本。
我们现在已经拥有了开始所需的一切。 接下来我们需要一个配置文件,让 TypeScript 知道如何处理我们的项目。 在您的存储库根目录中创建一个名为 tsconfig.json 的文件并粘贴以下内容:
{
"compilerOptions": {
"target": "ES2017",
"module": "commonjs",
"lib": ["es2017"],
"outDir": "dist",
"rootDir": "src",
"noImplicitAny": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
}
}
我们不会深入了解每个设置的作用,但是如果您想了解更多信息,可以运行 node_modules/.bin/tsc --init。 这将创建一个新的配置文件,其中列出了所有可用选项,并附有描述其功能的注释。
最后,我们将设置 TSLint。 这不是必需的,但这是一个很好的实践。 创建一个名为 tslint.json 的文件并粘贴以下内容:
{
"extends": "tslint-config-airbnb",
"rules": {
"import-name": false
}
}
这包括 Airbnb 规则的 TSLint 版本。我们唯一要删除的是导入名称规则,因为它可能非常严格。
我们快到了…!
设置 Nodemon 以实现无缝服务器重启
幸运的是,当我们使用 ts-node 时,这并不复杂,因为我们实际上不需要在每次重启之间编译 TypeScript。
为了在发生更改时重新启动我们的服务器,我们将使用 Nodemon 监视更改。 另一种选择是 PM2,这在 Docker 容器中开发时特别有用,但这是另一个时间的更大主题。
我的偏好是将其配置保存在一个单独的文件中; 创建一个 nodemon.json 文件并添加以下内容:
{
"watch": ["src"],
"exec": "npm run serve",
"ext": "ts"
}
这将监视 src 目录中 .ts 文件的任何更改,然后运行 npm run start。 这意味着我们需要在 package.json 中设置一个启动脚本。 打开你的 package.json 并添加以下脚本:
"scripts": {
"lint": "tslint --project tsconfig.json --format stylish",
"build": "tsc",
"serve": "ts-node src/server.ts",
"start": "nodemon"
}
在这里,我们向 lint 添加了一个脚本,以防您选择的编辑器没有自动执行(就个人而言,我使用带有 TSLint 扩展的 VSCode)。
我们还添加了一个通用的 serve 命令来运行服务器。 此命令不会监视更改或重新启动。
最后,我们使启动脚本直接指向 nodemon。 然后,Nodemon 将在任何更改时依次运行(并重新运行)npm serve。
如前所述,由于我们使用的是 ts-node,因此我们根本不需要编译。 也就是说,我们已经添加了一个构建命令。 这意味着您的项目可以通过 npm run SCRIPT(即 npm run lint)直接从 npm 运行。 我们也可以跳过启动脚本的运行部分,通过运行 npm start 来启动我们的项目。
###在 Docker 中设置 PostgreSQL(可选) 因为我们将使用 PostgreSQL 作为我们 API 的数据库,所以我们需要一个可用的安装。 为此,我们将使用 Docker。 如果你已经在本地安装了 PostgreSQL,或者你已经配置了一个外部服务,那么你可以跳过这一步。
创建一个 Docker Compose 文件。 在您的项目根目录中创建一个文件名 docker-compose.yml,并添加以下内容:
version: '3'
services:
database:
image: postgres:11-alpine
restart: always
expose:
- "5432"
ports:
- "5432:5432"
environment:
POSTGRES_DB: typescript-koa
adminer:
image: adminer:latest
restart: always
ports:
- "8080:8080"
environment:
ADMINER_DEFAULT_SERVER: database
ADMINER_DESIGN: lucas-sandery
这将使 PostgreSQL 在端口 5432 上本地可用,并且 Adminer 管理 GUI 在 http://127.0.0.1:8080 上可用,一旦容器启动,我们将在稍后进行。
现在我们准备好写一些代码了!
构建我们的 API
准备好 Koa
我们要做的第一件事是运行一个基本的 Koa 应用程序。 我们将要创建的应用程序将允许我们存储电影名称、发行日期和数字评级。
运行以下命令以创建应用程序文件:
mkdir -p src/app && touch src/app/app.ts
这是我们将在其中创建基本应用程序的文件。 在此文件中,添加以下代码:
import * as Koa from 'koa';
import * as HttpStatus from 'http-status-codes';
const app:Koa = new Koa();
// Generic error handling middleware.
app.use(async (ctx: Koa.Context, next: () => Promise<any>) => {
try {
await next();
} catch (error) {
ctx.status = error.statusCode || error.status || HttpStatus.INTERNAL_SERVER_ERROR;
error.status = ctx.status;
ctx.body = { error };
ctx.app.emit('error', error, ctx);
}
});
// Initial route
app.use(async (ctx:Koa.Context) => {
ctx.body = 'Hello world';
});
// Application error logging.
app.on('error', console.error);
export default app;
Koa 完全基于中间件。 上面的代码创建了一个 Koa 实例,并添加了一小块自定义中间件来稍微改进我们的错误日志记录。 对于实际应用程序,您可能需要更健壮的东西,但这对我们来说非常有用。
您会注意到我们也在导出应用程序。 这有两个目的:
- 它使我们的应用程序保持模块化,并且不会将我们的应用程序定义与服务器的运行联系起来。
- 它使我们能够更轻松地测试应用程序。
要启动我们的服务器运行,请在 src 目录中创建一个名为 server.ts 的文件:touch src/server.ts。
在这个文件中,我们将导入我们的应用程序并启动服务器。 为此,我们需要以下代码:
import app from './app/app';
// Process.env will always be comprised of strings, so we typecast the port to a
// number.
const PORT:number = Number(process.env.PORT) || 3000;
app.listen(PORT);
如果你现在运行 npm start,Nodemon 应该启动我们的服务器监听 3000 端口。如果你访问 127.0.0.1:3000 你应该会看到一个很好的“Hello world”。
这一切都很好,但是我们将如何创建具有单一端点的movies?
Adding our routes
要添加我们的路由,我们需要使用 koa-router。
Koa 不附带开箱即用的路由器,而是让您以比其他框架更加模块化的方式组合应用程序。 – Koajs.com
为了便于使用,我们将在单独的文件中创建我们的路由:
mkdir src/movie && touch src/movie/movie.controller.ts
打开这个文件,粘贴以下代码:
import * as Koa from 'koa';
import * as Router from 'koa-router';
const routerOpts: Router.IRouterOptions = {
prefix: '/movies',
};
const router: Router = new Router(routerOpts);
router.get('/', async (ctx:Koa.Context) => {
ctx.body = 'GET ALL';
});
router.get('/:movie_id', async (ctx:Koa.Context) => {
ctx.body = 'GET SINGLE';
});
router.post('/', async (ctx:Koa.Context) => {
ctx.body = 'POST';
});
router.delete('/:movie_id', async (ctx:Koa.Context) => {
ctx.body = 'DELETE';
});
router.patch('/:movie_id', async (ctx:Koa.Context) => {
ctx.body = 'PATCH';
});
export default router;
这定义了我们的路线。 你会注意到我们设置了 /movies 的前缀。 这样当我们将路由挂载到应用程序中时,我们根本不需要在应用程序级别添加任何配置。 如果我们想把这个功能拆分成它自己的包,或者移动它,我们可以移动它。
如果您使用过 Express,您可能能够理解正在发生的事情。 路由器对象上有一些方法代表将用于我们的 API 的 HTTP 动词,然后是回调。 新的可能是回调是异步函数。
通过利用异步函数,Koa 允许您放弃回调并大大增加错误处理。-- Koajs.com
接下来,我们需要让这个控制器对应用程序可用。 在 app.js 的顶部,导入电影控制器:
import movieController from '../movie/movie.controller';
然后删除我们默认的“Hello world”端点,并将其替换为以下内容:
// Route middleware.
app.use(movieController.routes());
app.use(movieController.allowedMethods());
.routes() 部分将路由中间件添加到应用程序,而 .allowedMethods() 函数将添加另一块中间件,以确保对不允许或未实现的方法给出正确的响应。
如果您现在使用上述任何 HTTP 动词向我们的 API 发出任何请求,您应该会收到一个响应,其中提到了您发出的请求的类型。
好东西! 现在我们需要添加我们的数据库后端。
实现我们的持久层
添加数据库连接
此部分需要 PostgreSQL 数据库。 如果您已经在上面设置了 dockerfile,那么现在是在新的终端窗口中运行 docker-compose up 的好时机。 如果您在其他地方创建了数据库,请确保您手头有凭据。
我们需要做的第一件事是建立我们的数据库连接。 运行以下命令以创建我们将在其中存储数据库凭据的文件:
mkdir src/database && touch src/database/database.connection.ts
打开此文件并粘贴以下内容:
import 'reflect-metadata';
import { createConnection, Connection, ConnectionOptions } from 'typeorm';
import { join } from 'path';
const parentDir = join(__dirname, '..');
const connectionOpts: ConnectionOptions = {
type: 'postgres',
host: process.env.DB_HOST || 'localhost',
port: Number(process.env.DB_PORT) || 5432,
username: process.env.DB_USERNAME || 'postgres',
password: process.env.DB_PASSWORD || 'postgres',
database: process.env.DB_NAME || 'typescript-koa',
entities: [
`${parentDir}/**/*.entity.ts`,
],
synchronize: true,
};
const connection:Promise<Connection> = createConnection(connectionOpts);
export default connection;
这个文件使用 TypeORM 创建我们的连接。 您可以在此处找到所有配置选项的文档。
我们也导出了这个,所以我们可以把它拉到我们的引导 server.ts 文件中。 如果您正在运行自己的 PostgreSQL 实例,则可以更改上面的默认连接详细信息。
我们设置它的方式允许环境变量成为默认的真实来源,并且我们有非敏感的本地凭据作为备份。
附带说明一下,如果您直接通过 NodeJS 运行应用程序,并将 .ts 文件转换为 JavaScript,则需要将 ${parentDir}/*/.entity.js 添加到实体列表中。
接下来,重新访问 server.ts 并将内容更改为以下内容:
import app from './app/app';
import databaseConnection from './database/database.connection';
const PORT:number = Number(process.env.PORT) || 3000;
databaseConnection
.then(() => app.listen(PORT))
.catch(console.error);
我们创建的连接返回一个承诺。 由于我们的应用程序依赖于一个数据库,我们可以安全地在数据库连接的成功回调中启动服务器。
定义数据模型
TypeORM 将其数据模型称为实体。 实体是用 TypeScript 装饰器包装以添加底层功能的类。 它与 [Doctrine](https://www.doctrine-project.org/ 在 PHP 中的工作方式非常相似。 我们的演示只需要一个实体,所以我们接下来要创建一个实体类:
touch src/movie/movie.entity.ts
在此文件中,粘贴以下代码:
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity()
export default class Movie {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
name: string;
@Column({ type: 'int', nullable: true, width: 4 })
releaseYear: number;
@Column({ type: 'int', nullable: true })
rating: number;
}
信不信由你,这是我们能够访问整个持久层所需的唯一数据。 查看官方文档以了解有关创建实体的所有不同选项的更多信息。
保存完所有内容并重新启动服务器后,您应该注意到我们的数据库中现在有一个电影表! 这意味着是时候更新我们的路线了。
持续更新路由
我们需要做的最后一件事是使用持久化、编辑和删除电影实体所需的功能更新我们的路由。
访问我们的原始 app.ts 文件,并在顶部粘贴以下内容以导入正文解析器:
import * as bodyParser from 'koa-bodyparser';
这是 Koa 能够读取请求正文所必需的。 在路由use 调用之前,我们还需要添加以下内容:
// Middleware
app.use(bodyParser());
再次打开 movie.controller.ts 并将所有内容替换为以下代码(这是一个相当大的片段,因此已添加注释以帮助说明发生了什么):
import * as Koa from 'koa';
import * as Router from 'koa-router';
import { getRepository, Repository } from 'typeorm';
import movieEntity from './movie.entity';
import * as HttpStatus from 'http-status-codes';
const routerOpts: Router.IRouterOptions = {
prefix: '/movies',
};
const router: Router = new Router(routerOpts);
router.get('/', async (ctx:Koa.Context) => {
// Get the movie repository from TypeORM.
const movieRepo:Repository<movieEntity> = getRepository(movieEntity);
// Find the requested movie.
const movies = await movieRepo.find();
// Respond with our movie data.
ctx.body = {
data: { movies },
};
});
router.get('/:movie_id', async (ctx:Koa.Context) => {
// Get the movie repository from TypeORM.
const movieRepo:Repository<movieEntity> = getRepository(movieEntity);
// Find the requested movie.
const movie = await movieRepo.findOne(ctx.params.movie_id);
// If the movie doesn't exist, then throw a 404.
// This will be handled upstream by our custom error middleware.
if (!movie) {
ctx.throw(HttpStatus.NOT_FOUND);
}
// Respond with our movie data.
ctx.body = {
data: { movie },
};
});
router.post('/', async (ctx:Koa.Context) => {
// Get the movie repository from TypeORM.
const movieRepo:Repository<movieEntity> = getRepository(movieEntity);
// Create our new movie.
const movie: movieEntity = movieRepo.create(ctx.request.body);
// Persist it to the database.
await movieRepo.save(movie);
// Set the status to 201.
// Respond with our movie data.ctx.status = HttpStatus.CREATED;
ctx.body = {
data: { movie },
};
});
router.delete('/:movie_id', async (ctx:Koa.Context) => {
// Get the movie repository from TypeORM.
const movieRepo:Repository<movieEntity> = getRepository(movieEntity);
// Find the requested movie.
const movie = await movieRepo.findOne(ctx.params.movie_id);
// If the movie doesn't exist, then throw a 404.
// This will be handled upstream by our custom error middleware.
if (!movie) {
ctx.throw(HttpStatus.NOT_FOUND);
}
// Delete our movie.
await movieRepo.delete(movie);
// Respond with no data, but make sure we have a 204 response code.
ctx.status = HttpStatus.NO_CONTENT;
});
router.patch('/:movie_id', async (ctx:Koa.Context) => {
// Get the movie repository from TypeORM.
const movieRepo:Repository<movieEntity> = getRepository(movieEntity);
// Find the requested movie.
const movie:movieEntity = await movieRepo.findOne(ctx.params.movie_id);
// If the movie doesn't exist, then throw a 404.
// This will be handled upstream by our custom error middleware.
if (!movie) {
ctx.throw(HttpStatus.NOT_FOUND);
}
// Merge the existing movie with the new data.
// This allows for really simple partial (PATCH).
const updatedMovie = await movieRepo.merge(movie, ctx.request.body);
// Save the new data.
movieRepo.save(updatedMovie);
// Respond with our movie data.// Response with the updated content.
ctx.body = {
data: { movie: updatedMovie },
};
});
export default router;
以上使 GET、POST、PATCH 和 DELETE 端点能够与我们的电影实体进行交互。
这应该就是我们需要的一切!
您可以通过发出 POST 请求来测试我们的新 API。
使用 Insomnia 或 Postman(或类似工具),使用以下数据构建请求并将其发送到 http://127.0.0.1:3000/movies:
{
"name": "Main in Manhattan",
"releaseYear": 2002,
"rating": 10
}
您应该返回电影实例。 记下 UUID,我们可以使用它来构建对 http://127.0.0.1:3000/movies/{UUID} 的 GET 请求。
总结
TypeScript 可以大大提高大中型应用程序的生产力。 从我的个人经验的角度来看,它减少了我必须编写的测试数量大约三分之一,并捕获类型错误——实际上是在我输入时,而不是在测试端点时。
我也是最近才尝试 TypeORM,从 Mongoose 转移过来。 虽然 Mongoose 很有趣并且使用起来相对简单,但是使用 TypeScript 启动和运行(以及一般情况下)需要很多样板。
TypeORM 是一股清新的空气,它的使用速度快得令人难以置信。 希望本教程有助于证明这一点。
您可以在此处找到上述文章的代码。
6666666666666666666
6666
666
@luo1234560 哈哈哈
666 不过生产环境最好转换成js文件再运行 在生产中使用 ts-node 是一种不好的做法吗?
@wangkunmeng 有道理