您的第一个使用 Typescript 的 Node Express 应用程序目标初始设置创建我们的 Express 应用程序超越“Hello World”探索 Express 类型应用程序代码结论

2025-06-10

您的第一个使用 Typescript 的 Node Express 应用程序

目标

初始设置

创建我们的 Express App

超越“Hello World”

导出 Express 类型

应用程序代码

结论

Express 是 Node.js 中最常用的框架。在本文中,我们将学习如何将 Typescript 添加到 Express 中。

目标

我们的目标是能够使用 Typescript 快速开发我们的应用程序,但最终我们希望我们的应用程序能够编译为普通的旧 javascript 以便由 nodejs 运行时执行。

初始设置

首先,我们需要创建一个应用程序目录来托管我们的应用文件。我们将其命名为express-typescript-app

mkdir express-typescript-app
cd express-typescript-app
Enter fullscreen mode Exit fullscreen mode

为了实现我们的目标,我们需要区分我们安装的常规应用程序依赖项和开发依赖项(即,可以帮助我们开发应用程序但在编译代码后不再需要的依赖项)。

在本教程中,我将使用yarn包管理器,但您也可以npm轻松使用!

生产依赖项

在生产环境中,这仍然是一个express应用程序。因此,我们需要安装 express!

yarn add express
Enter fullscreen mode Exit fullscreen mode

请注意,这将为package.json我们创建一个文件!

目前,这将是我们唯一的生产依赖项(我们稍后会添加另一个)。

开发依赖项

在开发环境中,我们将编写 Typescript。因此,我们需要安装typescript。我们还需要安装 express 和 node 的类型。我们使用-D标志来表明yarn这些是开发依赖项。

yarn add -D typescript @types/express @types/express @types/node
Enter fullscreen mode Exit fullscreen mode

太棒了!但我们还没完工。当然,我们可以就此打住,但问题是,每次开发过程中想要查看变更时,都需要重新编译代码。这可不是闹着玩的!所以我们需要添加一些额外的依赖项:

  • ts-node——这个包可以让我们无需编译即可运行 Typescript!这对于本地开发至关重要。
  • nodemon——此软件包会自动监视应用程序代码的更改,并重新启动您的开发服务器。配合使用ts-node,我们将能够立即nodemon看到应用程序中的更改

再次强调,这些是开发依赖项,因为它们仅帮助我们进行开发,并且在我们的代码编译用于生产后不会使用。

yarn add -D ts-node nodemon
Enter fullscreen mode Exit fullscreen mode

配置我们的应用程序以运行

配置 Typescript

由于我们使用的是 Typescript,因此我们先设置一些 Typescript 选项。我们可以在一个文件中进行设置tsconfig.json

touch tsconfig.json
Enter fullscreen mode Exit fullscreen mode

现在在我们的 Typescript 配置文件中,让我们设置一些编译器选项。

  • module: "commonjs"—当我们编译代码时,我们的输出将使用commonjs模块,如果我们以前使用过节点,我们就会熟悉它。
  • esModuleInterop: true—此选项允许我们进行星号(*)和默认导入。
  • target: "es6"与前端不同,我们可以控制运行时环境。我们将确保使用支持 ES6 标准的 Node 版本。
  • rootDir: "./"—我们的 Typescript 代码的根目录是当前目录。
  • outDir: "./build"—当我们将 Typescript 编译为 JavaScript 时,我们会将 JS 放在./build目录中。
  • strict: true— 启用严格的类型检查!

总的来说,我们的tsconfig.json文件应该是这样的:

{
  "compilerOptions": {
    "module": "commonjs",
    "esModuleInterop": true,
    "target": "es6",
    "rootDir": "./",
    "outDir": "./build",
    "strict": true
  }
}
Enter fullscreen mode Exit fullscreen mode

配置 package.json 脚本

目前,我们还没有package.json脚本!我们需要添加几个脚本:一个添加到start开发模式下的应用,另一个添加到build生产模式下的应用。要在开发模式下启动应用,我们只需运行nodemon index.ts。为了构建应用,我们已经在文件中为 Typescript 编译器提供了所需的所有信息tsconfig.json,因此我们只需运行tsc

下面展示了你的package.json文件现在的样子。请注意,由于我之前写过这篇文章,所以你的依赖项版本可能与我的不同(顺便说一下,来自过去的问候)。

{
  "dependencies": {
    "express": "^4.17.1"
  },
  "devDependencies": {
    "@types/express": "^4.17.11",
    "@types/node": "^14.14.21",
    "nodemon": "^2.0.7",
    "ts-node": "^9.1.1",
    "typescript": "^4.1.3"
  },
  "scripts": {
    "build": "tsc",
    "start": "nodemon index.ts"
  }
}
Enter fullscreen mode Exit fullscreen mode

Git 配置

如果您正在使用 git(我推荐它!),您会希望.gitignore文件忽略您的node_modules文件夹和您的build文件夹:

touch .gitignore
Enter fullscreen mode Exit fullscreen mode

文件内容如下:

node_modules
build
Enter fullscreen mode Exit fullscreen mode

设置完成!

希望你已经完成了设置!虽然还不错但比起普通的 express.js 应用,门槛确实略高一些。

创建我们的 Express App

让我们创建我们的 Express 应用。这实际上与我们使用普通的 JavaScript 的操作非常相似。唯一的区别是我们可以使用 ES6 导入!

让我们创建index.ts

touch index.ts
Enter fullscreen mode Exit fullscreen mode

在该index.ts文件中,我们可以做一个基本的“hello world”示例:

import express from 'express';

const app = express();
const PORT = 3000;

app.get('/', (req, res) => {
  res.send('Hello world');
});

app.listen(PORT, () => {
  console.log(`Express with Typescript! http://localhost:${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

现在,我们可以在终端中使用以下命令启动该应用程序yarn run start

yarn run start
Enter fullscreen mode Exit fullscreen mode

您将获得如下输出:

$ nodemon index.ts
[nodemon] 2.0.7
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: ts,json
[nodemon] starting `ts-node index.ts`
Express with Typescript! http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

我们可以看到nodemon它正在监视所有文件的更改,并使用 启动我们的应用程序ts-node index.ts。现在,我们可以在 Web 浏览器中导航到http://localhost:3000,并看到我们的“hello world”应用程序的全部功能!

浏览器显示“hello world”

好极了!(嗯,这是一个开始!)

超越“Hello World”

我们的“Hello world”应用已经很不错了,但我认为我们还可以做得更多。让我们创建一些(非常糟糕的)用户注册功能,稍微展示一下我们的 express/typescript 能力。具体来说,这个功能将:

  • 在内存中维护用户和相关密码的列表
  • 有一个POST允许用户注册的端点(即,向上述列表中添加一个额外的用户)
  • 拥有一个POST允许用户尝试登录的端点,并根据所提供凭证的正确性发出适当的响应

让我们开始吧!

维护用户

首先,我们创建一个types.ts文件来声明我们的User类型。以后我们会用这个文件来声明更多类型。

touch types.ts
Enter fullscreen mode Exit fullscreen mode

现在添加User类型types.ts并确保导出它:

export type User = { username: string; password: string };
Enter fullscreen mode Exit fullscreen mode

好的!所以我们不用数据库之类的花哨东西,直接把用户信息保存在内存中。让我们users.ts在新目录中创建一个文件data

mkdir data
touch data/users.ts
Enter fullscreen mode Exit fullscreen mode

现在在我们的users.ts文件中,我们可以创建一个空的用户数组,并确保将其指定为我们User类型的数组。

import { User } from "../types.ts;

const users: User[] = [];
Enter fullscreen mode Exit fullscreen mode

发布新用户

接下来,我们希望能够将POST新用户添加到我们的应用程序中。如果您熟悉 HTTP 的实际情况,您就会知道变量通常会出现在 HTTP 请求正文中,类似于 URL 编码的变量(例如username=foo&password=bar)。与其自己解析这些变量,不如使用无处不在的body-parser中间件。现在就安装它:

yarn add body-parser
Enter fullscreen mode Exit fullscreen mode

然后我们将在我们的应用程序中导入并使用它:

import express from 'express';
import bodyParser from 'body-parser';

const app = express();
const PORT = 3000;

app.use(bodyParser.urlencoded({ extended: false }));

app.get('/', (req, res) => {
  res.send('Hello world');
});

app.listen(PORT, () => {
  console.log(`Express with Typescript! http://localhost:${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

最后,我们可以在端点POST上创建一个请求处理程序/users。该处理程序将执行以下操作:

  • 检查请求主体中是否定义了username和,并对这些字段运行一些非常基本的验证password
  • 400如果提供的值有任何问题,则返回状态消息
  • 将新用户推送到我们的users数组
  • 返回201状态消息

让我们开始吧。首先,我们在文件addUser中创建一个函数data/users.ts

import { User } from '../types.ts';

const users: User[] = [];

const addUser = (newUser: User) => {
  users.push(newUser);
};
Enter fullscreen mode Exit fullscreen mode

现在,我们回到我们的index.ts文件并添加"/users"路线:

import express from 'express';
import bodyParser from 'body-parser';
import { addUser } from './data/users';

const app = express();
const PORT = 3000;

app.use(bodyParser.urlencoded({ extended: false }));

app.get('/', (req, res) => {
  res.send('Hello world');
});

app.post('/users', (req, res) => {
  const { username, password } = req.body;
  if (!username?.trim() || !password?.trim()) {
    return res.status(400).send('Bad username or password');
  }
  addUser({ username, password });
  res.status(201).send('User created');
});

app.listen(PORT, () => {
  console.log(`Express with Typescript! http://localhost:${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

这里的逻辑很简单:我们的usernamepassword变量必须存在,并且在使用该trim()方法时,它们的长度必须大于零个字符。如果不符合这些条件,我们将返回400一个带有自定义 Bad Request 消息的错误。否则,我们push将新的usernamepassword添加到users数组中并201返回状态。

注意:你可能会注意到,我们的用户数组无法知道用户名是否被重复添加。我们先假设我们的应用没有这个明显的问题!

让我们来测试一下这个注册逻辑curl!在你的终端中,发出以下 POST 请求:

curl -d "username=foo&password=bar" -X POST http://localhost:3000/users
Enter fullscreen mode Exit fullscreen mode

您应该会收到以下回复:

User created
Enter fullscreen mode Exit fullscreen mode

成功了!现在,让我们验证一下,如果请求不符合验证条件,请求是否会失败。我们将提供一个只包含一个空格的密码(" ".trim()如果为 false,则验证失败)。

curl -d "username=foo&password= " -X POST http://localhost:3000/users
Enter fullscreen mode Exit fullscreen mode

我们得到以下响应:

Bad username or password
Enter fullscreen mode Exit fullscreen mode

我觉得很好看!

登录

登录的过程非常类似。我们将从请求主体中获取提供的状态,并使用该username方法检查该用户名/密码组合是否存在于数组中,并返回一个表示用户已登录的状态或一个表示用户未通过身份验证的状态。passwordArray.findusers200401

首先,让我们getUserdata/users.ts文件添加一个函数:

import { User } from '../types';

const users: User[] = [];

export const addUser = (newUser: User) => {
  users.push(newUser);
};

export const getUser = (user: User) => {
  return users.find(
    (u) => u.username === user.username && u.password === user.password
  );
};
Enter fullscreen mode Exit fullscreen mode

getUser函数将返回数组user中的匹配项,或者在没有用户匹配时users返回。undefined

接下来,我们在文件中使用此getUser函数index.ts

import express from 'express';
import bodyParser from 'body-parser';
import { addUser, getUser } from "./data/users';

const app = express();
const PORT = 3000;

app.use(bodyParser.urlencoded({ extended: false }));

app.get('/', (req, res) => {
  res.send('Hello word');
});

app.post('/users', (req, res) => {
  const { username, password } = req.body;
  if (!username?.trim() || !password?.trim()) {
    return res.status(400).send('Bad username or password');
  }
  addUser({ username, password });
  res.status(201).send('User created');
});

app.post('/login', (req, res) => {
  const { username, password } = req.body;
  const found = getUser({username, password})
  if (!found) {
    return res.status(401).send('Login failed');
  }
  res.status(200).send('Success');
});

app.listen(PORT, () => {
  console.log(`Express with Typescript! http://localhost:${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

现在我们可以再次使用 curl 添加用户,以该用户身份登录,然后再次尝试登录:

curl -d "username=joe&password=hard2guess" -X POST http://localhost:3000/users
# User created

curl -d "username=joe&password=hard2guess" -X POST http://localhost:3000/login
# Success

curl -d "username=joe&password=wrong" -X POST http://localhost:3000/login
# Login failed
Enter fullscreen mode Exit fullscreen mode

嘿,我们做到了!

导出 Express 类型

你可能已经注意到,除了初始设置之外,我们目前所做的一切都是 Express 的基本操作。事实上,如果你之前用过很多 Express,你可能会觉得它有点无聊(抱歉)。

但现在我们要来点更有趣的:我们将探索 express 导出的一些类型。为此,我们将定义一个自定义结构来定义我们的路由、路由的中间件和处理函数。

自定义路由类型

也许我们想在我们的开发商店中建立一个标准,我们将所有路线都写成这样:

const route = {
  method: 'post',
  path: '/users',
  middleware: [middleware1, middleware2],
  handler: userSignup,
};
Enter fullscreen mode Exit fullscreen mode

我们可以通过在文件中定义一个Route类型来实现这一点types.ts。重要的是,我们将利用从express包中导出的一些重要类型:RequestResponseNextFunctionRequest对象表示来自客户端的请求,Response对象是 Express 发送的响应, 是函数NextFunction的签名(next()如果您使用过 Express 中间件,您可能对此很熟悉)。

在我们的types.ts文件中,让我们指定我们的。我们将数组和函数中Route自由地使用类型,因为我们稍后会进一步讨论它们。anymiddlewarehandler

export type User = { username: string; password: string };

type Method =
  | 'get'
  | 'head'
  | 'post'
  | 'put'
  | 'delete'
  | 'connect'
  | 'options'
  | 'trace'
  | 'patch';

export type Route = {
  method: Method;
  path: string;
  middleware: any[];
  handler: any;
};
Enter fullscreen mode Exit fullscreen mode

现在,如果您熟悉 express 中间件,您就会知道典型的中间件功能如下所示:

function middleware(request, response, next) {
  // Do some logic with the request
  if (request.body.something === 'foo') {
    // Failed criteria, send forbidden resposne
    return response.status(403).send('Forbidden');
  }
  // Succeeded, go to the next middleware
  next();
}
Enter fullscreen mode Exit fullscreen mode

事实证明,express 为中间件采用的三个参数分别导出类型:RequestResponseNextFunction。因此,我们可以Middleware根据需要创建一个类型:

import { Request, Response, NextFunction } from 'express';

type Middleware = (req: Request, res: Response, next: NextFunction) => any;
Enter fullscreen mode Exit fullscreen mode

……但事实证明 express 已经有一个名为 的类型了RequestHandler!我不喜欢这个类型的名字RequestHandler,所以我们继续用这个名字导入它Middleware,然后把它添加到我们的Route类型中types.ts

import { RequestHandler as Middleware } from 'express';

export type User = { username: string; password: string };

type Method =
  | 'get'
  | 'head'
  | 'post'
  | 'put'
  | 'delete'
  | 'connect'
  | 'options'
  | 'trace'
  | 'patch';

export type Route = {
  method: Method;
  path: string;
  middleware: Middleware[];
  handler: any;
};
Enter fullscreen mode Exit fullscreen mode

最后,我们需要定义handler函数的类型。这纯粹是个人偏好,因为从技术上讲,我们的处理程序可能是最后一个中间件,但也许我们做了一个设计决定,想将handler函数单独列出来。重要的是,我们不希望处理程序接受next参数;我们希望它作为行尾。因此,我们将创建自己的Handler类型。它看起来与 非常相似,RequestHandler但不会接受第三个参数。

import { Request, Response, RequestHandler as Middleware } from 'express';

export type User = { username: string; password: string };

type Method =
  | 'get'
  | 'head'
  | 'post'
  | 'put'
  | 'delete'
  | 'connect'
  | 'options'
  | 'trace'
  | 'patch';

type Handler = (req: Request, res: Response) => any;

export type Route = {
  method: Method;
  path: string;
  middleware: Middleware[];
  handler: Handler;
};
Enter fullscreen mode Exit fullscreen mode

添加一些结构

我们不需要将所有中间件和处理程序都放在index.ts文件中,而是添加一些结构。

处理程序

首先,让我们将与用户相关的处理函数移动到一个handlers目录中:

mkdir handlers
touch handlers/user.ts
Enter fullscreen mode Exit fullscreen mode

然后,在我们的handlers/user.ts文件中,我们可以添加以下代码。这代表我们文件中已有的一个与用户相关的路由处理程序(注册)index.ts,我们只是对其进行了重新组织。重要的是,我们可以确保该signup函数满足我们的需求,因为它与类型的类型签名匹配Handler

import { addUser } from '../data/users';
import { Handler } from '../types';

export const signup: Handler = (req, res) => {
  const { username, password } = req.body;
  if (!username?.trim() || !password?.trim()) {
    return res.status(400).send('Bad username or password');
  }
  addUser({ username, password });
  res.status(201).send('User created');
};
Enter fullscreen mode Exit fullscreen mode

接下来,让我们添加一个包含我们login函数的身份验证处理程序。

touch handlers/auth.ts
Enter fullscreen mode Exit fullscreen mode

以下是我们可以移动到文件的代码auth.ts

import { getUser } from '../data/users';
import { Handler } from '../types';

export const login: Handler = (req, res) => {
  const { username, password } = req.body;
  const found = getUser({ username, password });
  if (!found) {
    return res.status(401).send('Login failed');
  }
  res.status(200).send('Success');
};
Enter fullscreen mode Exit fullscreen mode

最后,我们将为我们的主路由添加一个处理程序(“Hello world”)。

touch handlers/home.ts
Enter fullscreen mode Exit fullscreen mode

这个很简单:

import { Handler } from '../types';

export const home: Handler = (req, res) => {
  res.send('Hello world');
};
Enter fullscreen mode Exit fullscreen mode

中间件

我们目前还没有任何自定义中间件,但让我们来改变这一点!首先,为我们的中间件添加一个目录:

mkdir middleware
Enter fullscreen mode Exit fullscreen mode

我们可以添加一个中间件来记录path客户端的访问。我们可以这样调用requestLogger.ts

touch middleware/requestLogger.ts
Enter fullscreen mode Exit fullscreen mode

在这个文件中,我们可以再次RequestHandler从 express 导入,以确保我们的中间件函数是正确的类型:

import { RequestHandler as Middleware } from 'express';

export const requestLogger: Middleware = (req, res, next) => {
  console.log(req.path);
  next();
};
Enter fullscreen mode Exit fullscreen mode

创建路线

现在我们有了新奇的Route类型,并且把handlersmiddleware组织到各自的空间里,让我们来写一些路线吧!我们将routes.ts在根目录创建一个文件。

touch routes.ts
Enter fullscreen mode Exit fullscreen mode

下面是该文件的示例。请注意,requestLogger为了演示效果,我只在其中一条路由中添加了中间件——否则,只记录一条路由的请求路径就毫无意义了!

import { login } from './handlers/auth';
import { home } from './handlers/home';
import { signup } from './handlers/user';
import { requestLogger } from './middleware/requestLogger';
import { Route } from './types';

export const routes: Route[] = [
  {
    method: 'get',
    path: '/',
    middleware: [],
    handler: home,
  },
  {
    method: 'post',
    path: '/users',
    middleware: [],
    handler: signup,
  },
  {
    method: 'post',
    path: '/login',
    middleware: [requestLogger],
    handler: login,
  },
];
Enter fullscreen mode Exit fullscreen mode

修改我们的 index.ts 文件

现在终于有成果了!我们可以大大简化index.ts文件。我们用一个简单的循环替换所有路由代码forEach,该循环使用我们指定的所有内容routes.ts将路由注册到 express 中。重要的是,Typescript 编译器很满意,因为我们的Route类型与相应的 express 类型匹配。

import express from 'express';
import bodyParser from 'body-parser';
import { routes } from './routes';

const app = express();
const PORT = 3000;

app.use(bodyParser.urlencoded({ extended: false }));

routes.forEach((route) => {
  const { method, path, middleware, handler } = route;
  app[method](path, ...middleware, handler);
});

app.listen(PORT, () => {
  console.log(`Express with Typescript! http://localhost:${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

哇,看起来太棒了!而且,重要的是,我们建立了一个类型安全的模式,可以用来指定路由、中间件和处理程序。

应用程序代码

如果您想查看最终的应用程序代码,请前往此处的 github 存储库

结论

好了,以上就是一次关于 Express 与 Typescript 的有趣探索!我们看到,它最基本的实现方式与典型的 express.js 项目并无二致。然而,现在您可以利用 Typescript 的强大功能,以一种类型安全的方式赋予项目所需的结构。

鏂囩珷鏉ユ簮锛�https://dev.to/nas5w/your-first-node-express-app-with-typescript-2jkm
PREV
简单的 React JS 和 MySQL 集成 - CRUD 应用程序(后端)
NEXT
60 行代码即可编写您的第一个 Deno 服务器 入门 指定域名 获取服务器库 添加路由 最后一步:日志中间件 Fin