作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
马科斯·恩里克·达席尔瓦
验证专家 在工程
12 的经验

马科斯在IT和开发领域有17年以上的经验. 他的爱好包括休息架构、敏捷开发方法和JS.

分享

如何在节点中编写休息 API.js?

在为休息 API构建后端时,表达.js通常是节点的首选.js框架. 同时它也支持构建静态HTML和模板, 在本系列中, 我们将专注于使用打印稿进行后端开发. 生成的休息 API将是任何前端框架或外部后端服务都能够查询的API.

你需要:

  • 具备JavaScript和打印稿的基本知识
  • 节点基础知识.js
  • 休息架构的基本知识(参见. 本节 我之前的休息 API文章(如果需要的话)
  • 已经安装好的节点.Js(最好是版本14以上)

在终端(或命令提示符)中,我们将为项目创建一个文件夹. 从该文件夹中运行 npm init. 这会产生 一些基本的节点.我们需要的Js项目文件.

接下来,我们将添加表达.Js框架和一些有用的库:

NPM I 表达调试Winston 表达- Winston cors

这些库是有充分理由的 节点.js开发人员 最喜欢的:

  • 调试 是一个我们将用来避免调用的模块吗 控制台.日志() 在开发我们的应用程序时. 这样,我们就可以在故障排除期间轻松地过滤调试语句. 它们也可以在生产环境中完全关闭,而不必手动删除.
  • 温斯顿负责记录请求到我们的API和返回的响应(和错误). 表达-温斯顿 直接与表达集成.. js,这样所有的标准api相关 温斯顿 日志代码已经完成.
  • cors 是一张快件吗.Js中间件允许我们启用 跨域资源共享. 没有这个, 我们的API只能从与后端完全相同的子域提供服务的前端使用.

我们的后端在运行时使用这些包. 但我们也需要安装一些 发展 打印稿配置的依赖项. 为此,我们将运行:

——save-dev @types/cors @types/表达 @types/调试 source-map-support tslint typescript

这些依赖项需要在我们的应用自己的代码中启用打印稿, 以及表达使用的类型.Js和其他依赖项. 当我们使用像WebStorm或VSCode这样的IDE时,通过允许我们在编码时自动完成一些函数方法,这可以节省很多时间.

中的最终依赖项 包.json 应该是这样的:

“依赖”:{
    “调试”:“^ 4.2.0",
    “表达”:“^ 4.17.1",
    :“表达-温斯顿 ^ 4.0.5",
    “温斯顿”:“^ 3.3.3",
    “歌珥”:“^ 2.8.5"
},
" devDependencies ": {
    “@types /歌珥”:“^ 2.8.7",
    “@types /调试”:“^ 4.1.5",
    “@types /表达”:“^ 4.17.2",
    :“source-map-support ^ 0.5.16",
    :“tslint ^ 6.0.0",
    :“打印稿^ 3.7.5"
}

现在我们已经安装了所有必需的依赖项,让我们开始构建自己的代码!

打印稿 休息 API项目结构

在本教程中,我们将创建三个文件:

  1. ./app.ts
  2. ./共同/共同.路线.配置.ts
  3. ./用户/用户.路线.配置.ts

项目结构的两个文件夹(常见的 and 用户)是有各自职责的独立模块. 从这个意义上说,我们最终将为每个模块提供以下部分或全部内容:

  • 路由配置 来定义API可以处理的请求
  • 服务 用于连接到数据库模型等任务, 做查询, 或者连接到特定请求所需的外部服务
  • 中间件 在路由的最终控制器处理它的细节之前运行特定的请求验证
  • 模型 用于定义与给定数据库模式匹配的数据模型,以方便数据存储和检索
  • 控制器 将路由配置与最终(在任何中间件之后)处理路由请求的代码分离, 必要时调用上述服务函数, 并给客户一个响应

这个文件夹结构提供了 基本休息 API设计,这是本教程系列其余部分的早期起点,并且足以开始练习.

打印稿中的通用路由文件

常见的 文件夹,我们创建 常见的.路线.配置.ts 文件看起来像下面这样:

从“表达”中输入表达;
导出类CommonRoutesConfig {
    应用:表达.应用程序;
    名称:字符串;

    构造函数(app:表达.应用程序,名称:字符串){
        这.App = App;
        这.Name = Name;
    }
    getName () {
        返回这.名称;
    }
}

这里我们创建路由的方式是可选的. 但是因为我们在用打印稿, 我们的路由场景是一个练习使用继承的机会 扩展 关键字,我们很快就会看到. 在这个项目中, 所有的路由文件都有相同的行为:它们都有一个名称(我们将使用它进行调试),并且可以访问主表达.js 应用程序 object.

现在,我们可以开始创建用户路由文件了. 在 用户 文件夹,我们来创建 用户.路线.配置.ts 然后开始这样编码:

导入CommonRoutesConfig../共同/共同.路线.配置”;
从“表达”中输入表达;

导出类UsersRoutes扩展CommonRoutesConfig {
    构造函数(app:表达.应用程序){
        超级(应用,“UsersRoutes”);
    }
}

在这里,我们导入 CommonRoutesConfig 类并将其扩展到我们的新类 UsersRoutes. 通过构造函数,我们将应用程序(main 表达.应用程序 对象)和UsersRoutes到的名称 CommonRoutesConfig的构造函数.

这个例子非常简单, 但是当扩展到创建多个路由文件时, 这将帮助我们避免重复代码.

假设我们想要在这个文件中添加新特性,比如日志记录. 我们可以添加必要的字段到 CommonRoutesConfig 类,然后是所有扩展的路由 CommonRoutesConfig 会有访问权限吗.

使用打印稿抽象函数实现跨类的类似功能

如果我们想拥有一些功能呢 类似的 在这些类之间(比如配置API端点), 但这需要为每个类提供不同的实现? 一种选择是使用打印稿的特性 抽象.

让我们创建一个非常简单的抽象函数 UsersRoutes 类(以及未来的路由类)将继承自 CommonRoutesConfig. 假设我们想强制所有的路由都有一个函数(这样我们就可以从公共构造函数调用它) 配置ureRoutes (). 我们将在这里声明每个路由类资源的端点.

要做到这一点,我们将添加三个快速的东西 常见的.路线.配置.ts:

  1. 关键字 摘要 对我们的 class 行,以启用该类的抽象.
  2. 在类的末尾声明一个新的函数, 摘要 配置ureRoutes (): 表达.应用程序;. 这强制进行任何类扩展 CommonRoutesConfig 提供与该签名匹配的实现(如果不匹配), 打印稿编译器会抛出一个错误.
  3. 打电话给 这.配置ureRoutes (); 在构造函数的末尾,因为我们现在可以确定这个函数将存在.

结果:

从“表达”中输入表达;
导出抽象类CommonRoutesConfig {
    应用:表达.应用程序;
    名称:字符串;

    构造函数(app:表达.应用程序,名称:字符串){
        这.App = App;
        这.Name = Name;
        这.配置ureRoutes ();
    }
    getName () {
        返回这.名称;
    }
    摘要 配置ureRoutes (): 表达.应用程序;
}

有了它,任何类扩展 CommonRoutesConfig 必须有一个函数调用 配置ureRoutes () 返回一个 表达.应用程序 object. 这意味着 用户.路线.配置.ts 需要更新:

导入CommonRoutesConfig../共同/共同.路线.配置”;
从“表达”中输入表达;

导出类UsersRoutes扩展CommonRoutesConfig {
    构造函数(app:表达.应用程序){
        超级(应用,“UsersRoutes”);
    }

    配置ureRoutes () {
        //(我们将在这里添加实际的路由配置)
        返回这.应用程序;
    }

}

回顾一下我们的作品:

我们首先进口的是 常见的.路线.配置 文件,然后 表达 模块. 然后我们定义 UserRoutes 类,说我们想要它扩展 CommonRoutesConfig 基类,这意味着我们承诺它将实现 配置ureRoutes ().

将信息发送到 CommonRoutesConfig 类,我们正在使用 构造函数 这个班的. 它期望收到 表达.应用程序 对象,我们将在下一步中更深入地描述它. 与 super (),我们传递给 CommonRoutesConfig的构造函数是应用程序和路由的名称,在这个场景中是UsersRoutes. (super (),反过来,将调用我们的实现 配置ureRoutes ().)

配置表达.. js用户端点的路由

The 配置ureRoutes () 函数是我们为休息 API的用户创建端点的地方. 在这里,我们将使用 应用程序 和它的 路线 来自表达的功能.js.

使用的想法 app.路线() 功能是避免代码重复, 这很容易,因为我们正在创建一个具有良好定义的资源的休息 API. 本教程的主要资源是 用户. 在这种情况下,我们有两种情况:

  • 当API调用者想要创建一个新用户或列出所有现有用户时, 端点最初应该只有 用户 在请求路径的末尾. (在本文中,我们不会讨论查询过滤、分页或其他此类查询.)
  • 当调用者想要对特定的用户记录执行特定的操作时, 请求的资源路径将遵循此模式 用户/:用户标识.

的方式 .路线() 快件作品.js让我们用一些优雅的链接来处理HTTP动词. 这是因为 .get (), .post ()等.的实例,都返回相同的 IRoute 这是第一个 .路线() 电话是. 最终的配置是这样的:

配置ureRoutes () {

    这.app.路线(' /用户)
        .(要求:表达.请求,res: 表达.响应) => {
            res.状态(200).send(' List of 用户 ');
        })
        .(要求:表达.请求,res: 表达.响应) => {
            res.状态(200).send(' Post to 用户 ');
        });

    这.app.路线(/用户/:userId)
        .(要求:表达.请求,res: 表达.响应,下一个:表达.NextFunction) => {
            //这个中间件函数在任何请求/用户/:userId之前运行
            //但是它还没有完成任何事情——
            //使用next()将控制传递给下面的下一个适用函数
            next ();
        })
        .(要求:表达.请求,res: 表达.响应) => {
            res.状态(200).GET请求id ${req.参数个数.userId}’);
        })
        .(要求:表达.请求,res: 表达.响应) => {
            res.状态(200).发送(' PUT请求id ${req.参数个数.userId}’);
        })
        .补丁(要求:表达.请求,res: 表达.响应) => {
            res.状态(200).发送('请求补丁id ${req.参数个数.userId}’);
        })
        .删除(要求:表达.请求,res: 表达.响应) => {
            res.状态(200).请求删除id ${req.参数个数.userId}’);
        });

    返回这.应用程序;
}

上面的代码允许任何休息 API客户端调用我们的 用户 端点带有 POST 或者一个 GET 请求. 类似地,它允许客户机调用我们的 /用户/:userId 端点带有 GET, PUT, 补丁, or 删除 请求.

但对于 /用户/:userId,还添加了通用中间件 所有() 函数,该函数将在任何 get (), put (), 补丁(), or delete () 功能. 当(在本系列后面)我们创建仅由经过身份验证的用户访问的路由时,此功能将非常有用.

你可能已经注意到了 .所有() 函数-与任何中间件一样-有三种类型的字段: 请求, 响应, NextFunction.

  • The 请求 是快车.js表示要处理的HTTP请求. 类的升级和扩展 本地节点.js 请求类型.
  • The 响应 表达也是如此.js表示HTTP响应,再次扩展 本地节点.js 响应类型.
  • 同样重要的是 NextFunction 作为回调函数,允许控制通过任何其他中间件函数. 一路走来, 在控制器最终向请求者发送响应之前,所有中间件将共享相同的请求和响应对象.

我们的节点.. js入口点文件 app.ts

现在我们已经配置了一些基本的路由框架, 我们将开始配置应用程序的入口点. 让我们创建 app.ts 文件放到项目文件夹的根目录,并以下面的代码开始:

从“表达”中输入表达;
从' HTTP '导入*作为HTTP;

从' Winston '输入* as Winston;
从表达-温斯顿中导入* as 表达Winston;
从'cors'中导入cors;
导入CommonRoutesConfig./共同/共同.路线.配置”;
导入{UsersRoutes}./用户/用户.路线.配置”;
从'调试'中导入调试;

在本文中,只有两个导入是新的:

  • http 是一个节点.js-native模块. 这是启动我们快车的必要条件.js应用程序.
  • 分析体 表达自带的中间件是什么.js. 它在控制转到我们自己的请求处理程序之前解析请求(在我们的示例中为JSON).

现在我们已经导入了文件,我们将开始声明我们想要使用的变量:

Const 应用:表达.应用程序 = 表达();
Const服务器:HTTP.服务器= http.create服务器(应用);
Const port = 3000;
const 路线: Array = [];
const 调试Log:调试.IDebugger = 调试('app');

The 表达() 函数返回主表达.Js应用程序对象,我们将在整个代码中传递该对象,首先将其添加到 http.服务器 object. (我们需要启动 http.服务器 配置我们的 表达.应用程序.)

我们将监听端口3000——这是打印稿 会自动推断 是一个 数量-而不是标准端口80 (HTTP)或443 (HTTPS),因为这些通常用于应用程序的前端.

为什么使用3000端口?

没有规则规定端口必须是3000 -如果未指定, 任意端口 但是在节点.js和表达.所以我们继续这个传统.

节点可以.. js与前端共享端口?

我们仍然可以在自定义端口本地运行, 即使我们希望后端响应标准端口上的请求. 这需要一个 反向代理 在端口80或443上接收特定域或子域的请求. 然后它会将它们重定向到我们的内部端口3000.

The 路线 Array将跟踪我们的路由文件,以便调试,我们将在下面看到.

最后, 调试Log 会变成一个类似的函数吗 控制台.log, 但更好的是:它更容易微调,因为它会自动作用域到我们想要调用的文件/模块上下文. (在这个例子中,当我们把它作为字符串传递给 调试() 构造函数.)

现在,我们已经准备好配置所有的表达了.js中间件模块和我们API的路由:

//我们在这里添加中间件,将所有传入请求解析为JSON 
app.使用(表达.json ());

//我们在这里添加中间件来允许跨域请求
app.使用(歌珥());

//这里我们正在准备表达Winston日志中间件配置,
//自动记录表达处理的所有HTTP请求.js
const 日志记录器Options: 表达Winston.LoggerOptions = {
    运输:[新的温斯顿.传输.控制台()),
    格式:温斯顿.格式.结合(
        温斯顿.格式.json (),
        温斯顿.格式.prettyPrint (),
        温斯顿.格式.Colorize ({all: true})
    ),
};

if (!过程.env.调试){
    日志记录器Options.meta = false; // when not 调试ging, log 请求s as one-liners
}

//使用上述配置初始化日志记录器
app.使用(表达Winston.记录器(日志记录器Options));

//这里我们将UserRoutes添加到数组中,
//发送表达后.Js的应用程序对象,以便将路由添加到我们的应用程序中!
路线.推动(新UsersRoutes (app));

//这是一个简单的路由,以确保一切正常工作
const runningMessage = '服务器运行在http://localhost:${端口}';
app.Get ('/', (req: 表达).请求,res: 表达.响应) => {
    res.状态(200).发送(runningMessage)
});

The 表达Winston.日志记录器 hook into 表达.Js,自动记录细节——通过与 调试-每个已完成的请求. 我们传递给它的选项将整齐地格式化并为相应的终端输出着色, 在调试模式下使用更详细的日志记录(默认值).

注意,我们必须定义我们的路由 我们建立了 表达Winston.日志记录器.

最后也是最重要的一点:

服务器.listen(port, () => {
    路线.forEach((路线: CommonRoutesConfig) => {
        为${路线 . net配置的路由.getName()}’);
    });
    //我们唯一的例外是避免控制台.Log(),因为我们
    //总是想知道服务器何时完成启动
    控制台.日志(runningMessage);
});

这实际上启动了我们的服务器. 一旦启动,节点.Js将运行我们的回调函数, 在调试模式下,哪个会报告到目前为止我们配置的所有路由的名称, 只是 UsersRoutes. 在那之后, 我们的回调通知我们后端已准备好接收请求, 即使在生产模式下运行.

更新 包.json 将打印稿转换为JavaScript并运行应用

现在我们的骨架已经准备好运行了, 我们首先需要一些样板文件配置来启用打印稿编译. 让我们添加文件 ts配置.json 在项目根目录下:

{
  " compilerOptions ": {
    “目标”:“es2016”,
    “模块”:“常见的js”,
    “outDir”:“./ dist ",
    “严格”:没错,
    “esModuleInterop”:没错,
    “inlineSourceMap”:真的
  }
}

然后我们只需要添加最后的润色 包.json 以以下脚本的形式:

"脚本":{
    “开始”:“tsc && 节点——unhandled-rejections =严格 ./ dist /应用程序.js",
    "调试": "出口 调试 =* . && NPM运行启动”,
    "test": "echo \"Error: no test specified\" && 退出1”
},

The test Script是一个占位符,我们将在本系列的后面部分替换它.

The tsc开始 script属于打印稿. 它负责将打印稿代码翻译成JavaScript,然后输出到 dist 文件夹. 然后,我们只需运行构建版本 节点 ./ dist /应用程序.js.

我们通过 ——unhandled-rejections =严格 到节点.js(即使是节点 . js).Js v16+)因为在实践中, 使用直接的“崩溃并显示堆栈”方法进行调试比使用 表达Winston.errorLogger object. 即使在生产环境中也是如此,让节点.Js继续运行,尽管未处理的拒绝可能会使服务器处于意外状态, 允许进一步(和更复杂)的bug发生.

The 调试 脚本调用 开始 脚本,但首先定义了 调试 环境变量. 这使得我们所有的 调试Log () 语句(以及表达的类似语句).Js本身,它使用相同的 调试 模块)将有用的详细信息输出到终端——这些详细信息(方便地)在使用标准在生产模式下运行服务器时被隐藏 npm开始.

尝试运行 NPM运行调试 你自己,然后把它和 npm开始 查看控制台输出的变化情况.

提示:您可以将调试输出限制为 app.ts 文件的 调试Log () 语句的使用 调试 =应用 而不是 调试 = *. The 调试 模块通常是相当灵活的,而这个特性 也不例外.

Windows用户可能需要更改 出口 to SET出口 是如何在Mac和Linux上运行的. 如果您的项目需要支持多种开发环境, 跨环境包 这里提供了一个简单的解决方案.

测试Live 表达.js后端

NPM运行调试 or npm开始 仍然运行,我们的休息 API将准备好服务端口3000上的请求. 此时,我们可以使用cURL, 邮递员, 失眠等. 测试后端.

因为我们只为用户资源创建了一个骨架, 我们可以简单地发送没有主体的请求,以查看一切都按预期工作. 例如:

curl——请求 GET 'localhost:3000/用户/12345'

我们的后端应该返回答案 请求id 12345的GET.

至于 POST荷兰国际集团(ing):

curl——请求 POST 'localhost:3000/用户' \
——data-raw”

这和我们构建骨架的所有其他类型的请求看起来非常相似.

为快速节点做好准备.使用打印稿开发休息 API

在本文中, 我们通过从头配置项目并深入了解表达的基础知识,开始创建休息 API.js框架. 然后,我们通过构建一个模式,迈出了掌握打印稿的第一步 UsersRoutesConfig 扩展 CommonRoutesConfig我们将在本系列的下一篇文章中重用该模式. 我们通过配置我们的 app.ts 进入我们的新路线,然后 包.json 用脚本来构建和运行我们的应用程序.

但即使是用表达制作的休息 API的基础.js和打印稿是相当复杂的. In 下一部分 本系列的, 我们专注于为用户资源创建适当的控制器,并深入研究一些有用的服务模式, 中间件, 控制器, 和模型.

完整的项目可用 GitHub上,本文结束时的代码参见 toptal-article-01 分支.

了解基本知识

  • 我可以在节点中使用打印稿吗.js?

    绝对! 对于流行的npm包(包括表达)来说,这是非常常见的.要有相应的打印稿类型定义文件. 节点就是这样.Js本身,加上包含的子组件,如调试包.

  • 是节点.js适合休息 api?

    Yes. 节点.js本身可以用来创建生产就绪的休息 api, 还有一些流行的框架,比如表达.Js来减少不可避免的样板文件.

  • 打印稿难学吗?

    不,对于那些有现代JavaScript背景的人来说,开始学习打印稿并不难. 对于那些有面向对象编程经验的人来说,这甚至更容易. 但掌握打印稿的所有细微差别和最佳实践需要时间,就像掌握任何技能一样.

  • 我应该使用打印稿吗?

    这取决于项目,但绝对推荐用于节点.js编程. 它是一种更具表现力的语言,用于在后端对现实世界的问题域进行建模. 这使代码更具可读性,并减少了出现bug的可能性.

  • 打印稿是用来做什么的?

    打印稿在任何有JavaScript的地方使用, 但它特别适合于大型应用程序. 它使用JavaScript作为基础, 增加了静态类型和对面向对象编程(OOP)范例更好的支持. 这反过来又支持更高级的开发和调试体验.

聘请Toptal这方面的专家.
现在雇佣
马科斯·恩里克·达席尔瓦

马科斯·恩里克·达席尔瓦

验证专家 在工程
12 的经验

莱科,意大利莱科省

2017年2月25日成为会员

作者简介

马科斯在IT和开发领域有17年以上的经验. 他的爱好包括休息架构、敏捷开发方法和JS.

作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

世界级的文章,每周发一次.

输入您的电子邮件,即表示您同意我们的 隐私政策.

世界级的文章,每周发一次.

输入您的电子邮件,即表示您同意我们的 隐私政策.

Toptal开发者

加入总冠军® 社区.