Node.js Express TypeScript 基础教程


Node.js + Express + TypeScript 基础入门:构建健壮高效的 Web 应用

在现代 Web 开发领域,Node.js 以其非阻塞 I/O 和事件驱动的特性,成为了构建高性能后端服务的热门选择。Express.js 作为 Node.js 生态中最流行、简洁且灵活的 Web 应用框架,极大地简化了 Web 服务器和 API 的开发。而 TypeScript,作为 JavaScript 的超集,通过引入静态类型检查和最新的 ECMAScript 特性,显著提高了代码的可维护性、可读性和健壮性,特别是在大型项目中。

将这三者结合起来——Node.js 提供运行时环境,Express 提供 Web 框架结构,TypeScript 提供强大的类型系统和开发工具支持——能够帮助我们构建出类型安全、易于维护且高效的现代 Web 应用程序。本教程旨在为初学者提供一个详细的入门指南,涵盖环境搭建、项目初始化、基本路由、中间件、类型应用以及项目构建等核心环节。

目标读者:

  • 了解基本的 JavaScript 语法。
  • 对 Web 开发有初步认识(了解 HTTP 请求/响应)。
  • 希望学习如何使用 Node.js、Express 和 TypeScript 构建后端服务。

你将学到:

  1. Node.js、Express 和 TypeScript 的基本概念及其结合使用的优势。
  2. 如何搭建 Node.js + TypeScript 开发环境。
  3. 如何初始化一个 Express + TypeScript 项目。
  4. 使用 TypeScript 编写基本的 Express 路由。
  5. 理解并使用 Express 中间件。
  6. 在 Express 应用中有效利用 TypeScript 的类型系统。
  7. 如何处理请求参数、查询参数和请求体。
  8. 如何组织和模块化路由。
  9. 配置和使用环境变量。
  10. 如何构建 TypeScript 项目为可部署的 JavaScript 代码。

1. 基础概念

在深入实践之前,我们先简单了解一下这三个核心技术:

  • Node.js: 一个基于 Chrome V8 引擎的 JavaScript 运行时环境。它允许你在服务器端运行 JavaScript 代码。其关键特性是异步非阻塞 I/O,非常适合处理大量并发连接,适用于构建实时应用、API 服务等。
  • Express.js: 一个基于 Node.js 平台的极简、灵活的 Web 应用开发框架。它提供了一系列强大的特性,用于创建 Web 和移动应用程序,如路由、中间件支持、模板引擎集成等。Express 本身不强制任何特定的项目结构,给予开发者很大的自由度。
  • TypeScript: 由 Microsoft 开发和维护的一种开源编程语言。它是 JavaScript 的一个严格语法超集,并添加了可选的静态类型。TypeScript 代码最终会被编译成纯 JavaScript 代码,可以在任何支持 JavaScript 的环境中运行。它带来的主要好处包括:
    • 静态类型检查: 在编译时捕获类型错误,减少运行时 Bug。
    • 代码智能提示与自动完成: 提升开发效率和体验。
    • 更好的代码可读性和可维护性: 类型注解使代码意图更清晰。
    • 支持最新的 ECMAScript 特性: 可以使用 ES6+ 的新语法,并编译到兼容的目标版本。
    • 强大的面向对象编程支持: 类、接口、泛型等。

为什么结合使用?

  • 类型安全: TypeScript 为动态类型的 JavaScript 带来了静态类型检查,极大地减少了因类型错误导致的 bug,尤其是在涉及复杂数据结构和多人协作的大型项目中。
  • 开发效率: 配合 VS Code 等现代 IDE,TypeScript 的类型推断和智能提示能显著提高编码速度和准确性。重构代码也变得更加安全可靠。
  • 可维护性: 类型注解如同代码的文档,使得理解和维护现有代码库更加容易。
  • 生态系统: Node.js 和 Express 拥有庞大成熟的生态系统,而 TypeScript 与之无缝集成,并为许多流行的 npm 包提供了类型定义文件 (@types/*)。

2. 环境准备

在开始之前,请确保你的开发环境中安装了以下软件:

  1. Node.js 和 npm (Node Package Manager):

    • 访问 Node.js 官网 下载并安装适合你操作系统的 LTS (长期支持) 版本。npm 会随 Node.js 一起安装。
    • 安装完成后,在终端或命令提示符中运行以下命令验证安装:
      bash
      node -v
      npm -v

      如果能看到版本号输出,则表示安装成功。
  2. 代码编辑器:

    • 推荐使用 Visual Studio Code (VS Code),它对 TypeScript 提供了出色的内置支持。当然,你也可以使用其他你喜欢的编辑器,如 WebStorm、Sublime Text 等。

3. 项目初始化与配置

现在,让我们开始创建一个新的 Node.js + Express + TypeScript 项目。

3.1 创建项目目录并初始化 npm

打开你的终端,创建一个新的项目文件夹,并进入该文件夹:

bash
mkdir my-express-ts-app
cd my-express-ts-app

使用 npm 初始化项目,生成 package.json 文件。-y 标志会使用默认配置快速完成初始化:

bash
npm init -y

package.json 文件用于管理项目的依赖、脚本、元数据等信息。

3.2 安装核心依赖

我们需要安装 Express 作为 Web 框架。同时,因为我们使用 TypeScript,还需要安装 TypeScript 本身以及 Node.js 和 Express 的类型定义文件。类型定义文件(通常在 @types 命名空间下)能让 TypeScript 理解第三方 JavaScript 库的 API 和类型。

  • 安装 Express:
    bash
    npm install express
  • 安装 TypeScript 及相关类型定义 (作为开发依赖):
    TypeScript 是开发时工具,类型定义也是开发时需要,所以将它们安装为开发依赖 (--save-dev-D)。
    bash
    npm install --save-dev typescript @types/node @types/express

    • typescript: TypeScript 编译器 (tsc)。
    • @types/node: Node.js 核心 API 的类型定义。
    • @types/express: Express 框架的类型定义。

3.3 配置 TypeScript (tsconfig.json)

TypeScript 项目需要一个 tsconfig.json 文件来配置编译选项。你可以手动创建这个文件,或者使用 TypeScript 编译器命令来生成一个默认的配置文件:

bash
npx tsc --init

这条命令会在项目根目录下生成一个 tsconfig.json 文件,里面包含了很多配置项及其注释说明。对于一个基础的 Express 项目,我们建议修改或确认以下几个关键配置:

```jsonc
// tsconfig.json
{
"compilerOptions": {
/ 基本选项 /
"target": "ES2016", // 指定 ECMAScript 目标版本 (编译后的 JS 版本)
"module": "CommonJS", // 指定使用哪种模块系统 (Node.js 通常使用 CommonJS)
"outDir": "./dist", // 指定编译后输出的 JS 文件目录
"rootDir": "./src", // 指定 TypeScript 源码文件根目录

/* 严格类型检查选项 */
"strict": true, // 启用所有严格类型检查选项
// "noImplicitAny": true, // 不允许隐式的 any 类型
// "strictNullChecks": true, // 严格的 null 检查

/* 模块解析选项 */
"moduleResolution": "node", // 模块解析策略 (node 适用于 Node.js)
"baseUrl": ".", // 计算非绝对模块名的基准目录 (可选)
"paths": {}, // 模块名到基于 baseUrl 的路径映射 (可选)
"esModuleInterop": true, // 允许 CommonJS 和 ES 模块之间的互操作性,简化 import

/* 高级选项 */
"skipLibCheck": true, // 跳过声明文件的类型检查 (可以加快编译速度)
"forceConsistentCasingInFileNames": true // 强制文件名大小写一致

},
"include": ["src//*"], // 指定需要编译的文件或目录 (src 目录下所有 ts 文件)
"exclude": ["node_modules", "
/*.spec.ts"] // 指定排除编译的文件或目录
}
```

重要配置说明:

  • target: 决定编译后的 JavaScript 版本。ES2016 是一个比较安全的选择,兼容性较好。
  • module: Node.js 普遍使用 CommonJS 模块系统 (require/module.exports),所以设为 CommonJS
  • outDir: 编译后的 JavaScript 文件将放在 dist 目录下。
  • rootDir: 我们的 TypeScript 源代码将放在 src 目录下。
  • strict: 强烈建议启用,它能开启一系列严格的类型检查规则,帮助捕获更多潜在错误。
  • esModuleInterop: 启用后,可以更方便地导入像 Express 这样的 CommonJS 模块(例如使用 import express from 'express' 而不是 import * as express from 'express')。
  • include: 告诉 TypeScript 编译器只编译 src 目录下的文件。

3.4 创建项目结构

根据 tsconfig.json 的配置,我们创建 src 目录来存放 TypeScript 源代码:

bash
mkdir src

3.5 配置 package.json 脚本

为了方便开发和构建,我们在 package.json 文件中添加一些 npm 脚本:

json
// package.json (部分)
{
// ... 其他配置 ...
"main": "dist/server.js", // 项目入口文件指向编译后的 JS 文件
"scripts": {
"build": "tsc", // 编译 TypeScript 代码
"start": "node dist/server.js", // 运行编译后的 JavaScript 代码 (生产环境)
"dev": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/server.ts" // 开发模式:使用 ts-node 直接运行 TS,并用 nodemon 监听文件变化自动重启
"test": "echo \"Error: no test specified\" && exit 1"
},
// ... 其他配置 ...
}

  • main: 指向项目编译后的主入口文件,通常是 dist 目录下的启动文件。
  • build: 运行 tsc 命令,根据 tsconfig.json 配置将 src 目录下的 TypeScript 文件编译成 JavaScript 文件到 dist 目录。
  • start: 使用 Node.js 运行 dist 目录下编译好的 server.js 文件。这是生产环境中启动服务的方式。
  • dev: 用于开发环境。这里我们使用了两个有用的开发工具:
    • ts-node: 允许你直接运行 TypeScript 文件而无需先手动编译。
    • nodemon: 监视项目文件的变化,当检测到文件修改时自动重启 Node.js 应用。
    • 这条命令的意思是:使用 nodemon 监视 src 目录下的所有 .ts 文件,当文件变化时,执行 ts-node src/server.ts 来重新启动服务。

安装开发依赖 ts-nodenodemon:

bash
npm install --save-dev ts-node nodemon

现在,我们的项目基础结构和配置已经准备就绪。


4. 创建第一个 Express 服务器 (使用 TypeScript)

src 目录下,创建一个名为 server.ts 的文件,这将是我们的应用入口文件。

```typescript
// src/server.ts

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

// 创建 Express 应用实例
const app: Application = express();

// 定义端口号
const port: number = parseInt(process.env.PORT || '3000', 10);

// 一个简单的 GET 路由
app.get('/', (req: Request, res: Response) => {
res.send('Hello World from Express + TypeScript!');
});

// 启动服务器监听指定端口
app.listen(port, () => {
console.log(Server is running at http://localhost:${port});
});
```

代码解析:

  1. import express, { Request, Response, NextFunction, Application } from 'express';
    • 我们使用 ES6 的 import 语法导入 express 模块。由于在 tsconfig.json 中设置了 esModuleInterop: true,我们可以使用默认导入 import express from 'express'
    • 同时,我们从 express 模块中导入了几个重要的类型:
      • Application: Express 应用实例的类型。
      • Request: HTTP 请求对象的类型。
      • Response: HTTP 响应对象的类型。
      • NextFunction: 中间件中用于将控制权传递给下一个中间件的函数的类型(后面会用到)。
  2. const app: Application = express();
    • 调用 express() 函数创建一个 Express 应用实例。
    • 我们显式地给 app 变量添加了 Application 类型注解,增强了代码的可读性和类型检查。
  3. const port: number = parseInt(process.env.PORT || '3000', 10);
    • 定义服务器监听的端口号。我们尝试从环境变量 process.env.PORT 获取端口(这在部署时很有用),如果环境变量未设置,则默认使用 3000 端口。
    • parseInt() 用于将可能存在的字符串类型的端口号转换为数字。
    • 我们给 port 变量添加了 number 类型注解。
  4. app.get('/', (req: Request, res: Response) => { ... });
    • 定义了一个处理 HTTP GET 请求的路由。
    • 第一个参数是路径 ('/',表示根路径)。
    • 第二个参数是一个回调函数(也称为路由处理器),当匹配到该路径的 GET 请求时执行。
    • 回调函数接收两个主要的参数:req (请求对象) 和 res (响应对象)。
    • 我们为 reqres 添加了从 express 导入的 RequestResponse 类型注解。这使得在函数体内部访问 reqres 的属性和方法时,能获得 TypeScript 的类型检查和智能提示。
    • res.send(...): 向客户端发送响应。
  5. app.listen(port, () => { ... });
    • 启动 Express 应用,使其开始监听指定端口上的 HTTP 连接。
    • 第一个参数是端口号。
    • 第二个参数是一个回调函数,在服务器成功启动并开始监听后执行。我们通常在这里打印一条日志信息。

运行开发服务器:

现在,在终端中运行我们之前定义的 dev 脚本:

bash
npm run dev

如果一切顺利,你应该会看到类似以下的输出:

[nodemon] starting `ts-node src/server.ts`
Server is running at http://localhost:3000

打开你的浏览器或使用 curl 等工具访问 http://localhost:3000,你应该能看到 "Hello World from Express + TypeScript!" 的响应。

并且,如果你现在修改 src/server.ts 文件并保存,nodemon 会自动检测到变化并重启服务器,无需手动停止和启动,极大地提高了开发效率。


5. 深入路由与请求处理

一个 Web 应用的核心功能是处理来自客户端的不同请求(不同的 URL 和 HTTP 方法)。

5.1 基本路由方法

Express 提供了对应于 HTTP 方法的路由方法:

  • app.get(path, handler): 处理 GET 请求
  • app.post(path, handler): 处理 POST 请求
  • app.put(path, handler): 处理 PUT 请求
  • app.delete(path, handler): 处理 DELETE 请求
  • app.patch(path, handler): 处理 PATCH 请求
  • app.all(path, handler): 处理所有 HTTP 方法的请求

示例:添加一个 POST 路由

```typescript
// src/server.ts (在 app.get('/') 下方添加)

// 中间件:解析 JSON 格式的请求体
// Express v4.16.0+ 内置了 express.json() 和 express.urlencoded()
app.use(express.json()); // 用于解析 application/json
app.use(express.urlencoded({ extended: true })); // 用于解析 application/x-www-form-urlencoded

// 定义一个接口来描述 POST 请求体的数据结构
interface CreateUserPayload {
username: string;
email: string;
}

// POST 路由示例:创建用户
app.post('/users', (req: Request<{}, {}, CreateUserPayload>, res: Response) => {
// 使用类型注解,可以安全地访问 req.body 的属性
const { username, email } = req.body;

if (!username || !email) {
return res.status(400).json({ message: 'Username and email are required' });
}

// 在实际应用中,这里会将用户信息保存到数据库
console.log('Creating user:', { username, email });

// 返回成功响应
res.status(201).json({
message: 'User created successfully',
user: { id: Date.now(), username, email }, // 模拟生成一个用户 ID
});
});
```

代码解析:

  1. app.use(express.json());app.use(express.urlencoded({ extended: true }));
    • 这是中间件 (Middleware) 的使用。express.json()express.urlencoded() 是 Express 内置的中间件,用于解析请求体 (request body)。
    • express.json() 解析 Content-Type: application/json 的请求体,并将解析后的 JSON 数据挂载到 req.body 上。
    • express.urlencoded() 解析 Content-Type: application/x-www-form-urlencoded 的请求体(常用于 HTML 表单提交),并将解析后的数据挂载到 req.body 上。extended: true 允许解析嵌套对象。
    • 必须在需要访问 req.body 的路由之前使用这些中间件。
  2. interface CreateUserPayload { ... }
    • 我们使用 TypeScript 的 interface 定义了一个类型 CreateUserPayload,用于描述我们期望从 /users POST 请求的请求体中接收到的数据结构。这提供了类型安全和代码提示。
  3. app.post('/users', (req: Request<{}, {}, CreateUserPayload>, res: Response) => { ... });
    • 定义了一个处理 /users 路径的 POST 请求的路由。
    • 类型注解 Request<{}, {}, CreateUserPayload>: 这是 Express 中为 Request 对象添加更精细类型注解的方式。Request 类型是一个泛型,可以接收几个类型参数:Request<P, ResBody, ReqBody, ReqQuery>
      • P: 路由参数 (Params) 的类型,这里是 {} 表示没有路由参数。
      • ResBody: 响应体 (Response Body) 的类型,这里是 {} (或者 any),因为我们不在请求对象上定义响应体。
      • ReqBody: 请求体 (Request Body) 的类型,这里我们传入了 CreateUserPayload 接口。
      • ReqQuery: 查询参数 (Query Parameters) 的类型,这里是 {} 表示没有或不关心查询参数类型。
    • 通过为 ReqBody 指定 CreateUserPayload 类型,TypeScript 现在知道 req.body 应该具有 usernameemail 属性(都是 string 类型)。如果你试图访问 req.body.nonExistentProperty,TypeScript 会报错。
    • 我们在函数体内解构 req.body 获取 usernameemail,并进行了简单的校验。
    • res.status(400).json(...): 设置 HTTP 状态码为 400 (Bad Request) 并发送一个 JSON 响应。
    • res.status(201).json(...): 设置 HTTP 状态码为 201 (Created) 并发送一个包含成功信息和模拟创建的用户数据的 JSON 响应。

测试 POST 路由:

你可以使用 Postman、Insomnia 或 curl 等工具来测试这个 POST 路由。

使用 curl:

bash
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"username": "alice", "email": "[email protected]"}'

你应该会收到类似以下的 JSON 响应:

json
{
"message": "User created successfully",
"user": {
"id": 1678886400000, // 时间戳 ID
"username": "alice",
"email": "[email protected]"
}
}

并且在服务器的控制台会看到 Creating user: { username: 'alice', email: '[email protected]' } 的日志。

5.2 路由参数 (Route Parameters)

路由参数用于捕获 URL 中特定段的值。它们由路径中的冒号 (:) 后跟参数名称来定义。

示例:获取特定用户信息的 GET 路由

```typescript
// src/server.ts

// 定义路由参数的类型接口 (可选但推荐)
interface UserParams {
userId: string; // 路由参数默认是 string 类型
}

// GET 路由:获取特定用户信息
app.get('/users/:userId', (req: Request, res: Response) => {
// 通过 req.params 访问路由参数
const userId: string = req.params.userId;

// 检查 userId 是否是有效的数字 (示例性转换和检查)
const id = parseInt(userId, 10);
if (isNaN(id)) {
return res.status(400).json({ message: 'Invalid user ID format' });
}

// 在实际应用中,这里会根据 userId 从数据库查询用户信息
console.log('Fetching user with ID:', id);

// 模拟找到用户
const user = {
id: id,
username: user_${id},
email: user_${id}@example.com,
};

res.json(user);
});
```

代码解析:

  1. interface UserParams { userId: string; }
    • 定义了一个接口 UserParams 来描述路由参数的结构。这使得访问 req.params 时更加类型安全。
  2. app.get('/users/:userId', (req: Request<UserParams>, res: Response) => { ... });
    • 路径 /users/:userId 定义了一个名为 userId 的路由参数。
    • 类型注解 Request<UserParams>: 我们将 UserParams 作为第一个类型参数传递给 Request 泛型,表明我们期望 req.params 对象的类型是 UserParams
    • const userId: string = req.params.userId;: 通过 req.params 对象访问路由参数。由于类型注解,TypeScript 知道 req.params 上有一个 userId 属性,并且是 string 类型。
    • 注意:即使 URL 中的参数看起来像数字(如 /users/123),req.params 中的值始终是字符串类型。如果需要数字,你需要手动转换(如使用 parseInt)。
    • 我们添加了简单的验证逻辑,检查 userId 是否能成功转换为数字。

测试带参数的 GET 路由:

访问 http://localhost:3000/users/123,你应该看到类似以下的 JSON 响应:

json
{
"id": 123,
"username": "user_123",
"email": "[email protected]"
}

访问 http://localhost:3000/users/abc,你应该看到:

json
{
"message": "Invalid user ID format"
}

5.3 查询参数 (Query Parameters)

查询参数是 URL 中问号 (?) 之后的部分,用于传递可选的键值对数据(例如 /search?q=typescript&limit=10)。在 Express 中,它们可以通过 req.query 对象访问。

示例:带有查询参数的搜索路由

```typescript
// src/server.ts

// 定义查询参数的类型接口 (可选但推荐)
// 注意:查询参数的值可能是 string | string[] | ParsedQs | ParsedQs[]
// 为了简单起见,我们先假设它们是 string 或 undefined
interface SearchQuery {
q?: string; // 搜索关键词 (可选)
limit?: string; // 限制数量 (可选)
}

// GET 路由:搜索功能
app.get('/search', (req: Request<{}, {}, {}, SearchQuery>, res: Response) => {
// 通过 req.query 访问查询参数
const searchTerm = req.query.q;
const limit = req.query.limit ? parseInt(req.query.limit, 10) : 10; // 提供默认值

if (!searchTerm) {
return res.status(400).json({ message: 'Search query parameter "q" is required' });
}

if (isNaN(limit) || limit <= 0) {
return res.status(400).json({ message: 'Invalid limit parameter' });
}

console.log(Searching for "${searchTerm}" with limit ${limit});

// 模拟搜索结果
const results = Array.from({ length: Math.min(limit, 5) }, (_, i) => ({ // 最多返回 5 条
id: i + 1,
title: Result ${i + 1} for "${searchTerm}",
}));

res.json({
query: searchTerm,
limit: limit,
results: results,
});
});
```

代码解析:

  1. interface SearchQuery { q?: string; limit?: string; }
    • 定义了 SearchQuery 接口来描述我们期望的查询参数。? 表示这些参数是可选的。
    • 重要提示: Express 解析查询参数时,其值的类型比较复杂(可能是一个字符串、一个字符串数组,或者来自 qs 库解析后的对象)。为了简化基础教程,我们这里假设它们是字符串或 undefined。在实际应用中,你可能需要更健壮的处理或使用像 zod 这样的验证库来确保类型和格式。
  2. app.get('/search', (req: Request<{}, {}, {}, SearchQuery>, res: Response) => { ... });
    • 类型注解 Request<{}, {}, {}, SearchQuery>: 我们将 SearchQuery 作为第四个类型参数传递给 Request 泛型,表明 req.query 对象的类型是 SearchQuery
    • const searchTerm = req.query.q;: 访问查询参数 q。由于接口中 q 是可选的 (string | undefined),TypeScript 会正确提示。
    • const limit = req.query.limit ? parseInt(req.query.limit, 10) : 10;: 访问查询参数 limit,如果存在则尝试转换为数字,否则使用默认值 10。
    • 同样,我们添加了对查询参数的验证。

测试带查询参数的 GET 路由:

  • 访问 http://localhost:3000/search?q=typescript&limit=3
  • 访问 http://localhost:3000/search?q=node (使用默认 limit)
  • 访问 http://localhost:3000/search (缺少 q 参数,会返回 400 错误)
  • 访问 http://localhost:3000/search?q=test&limit=abc (limit 无效,会返回 400 错误)

6. 中间件 (Middleware)

中间件是 Express 框架的核心概念之一。它们是函数,可以访问请求对象 (req)、响应对象 (res) 以及应用程序请求-响应周期中的下一个中间件函数 (next)。

中间件的功能:

  • 执行任何代码。
  • 修改请求和响应对象。
  • 结束请求-响应周期(通过发送响应)。
  • 调用堆栈中的下一个中间件 (next())。

中间件的类型:

  • 应用级中间件: 使用 app.use()app.METHOD() 绑定到 app 实例。
  • 路由级中间件: 类似于应用级,但绑定到 express.Router() 实例。
  • 错误处理中间件: 具有特殊签名 (err, req, res, next),用于捕获和处理路由及其他中间件中发生的错误。
  • 内置中间件:express.json(), express.urlencoded(), express.static()
  • 第三方中间件: 从 npm 安装的包,如 cors, helmet, morgan 等。

6.1 创建自定义中间件

让我们创建一个简单的日志记录中间件,记录每个接收到的请求。

```typescript
// src/server.ts

// ... (之前的 import 和 app 创建) ...

// 自定义日志中间件
const requestLoggerMiddleware = (req: Request, res: Response, next: NextFunction) => {
console.log([${new Date().toISOString()}] ${req.method} ${req.originalUrl});
// 调用 next() 将控制权传递给下一个中间件或路由处理器
next();
};

// 应用级中间件:在所有路由之前使用日志中间件
app.use(requestLoggerMiddleware);

// 应用级中间件:解析 JSON 和 URL-encoded 请求体 (之前已添加)
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// --- 路由定义 ---
app.get('/', (req: Request, res: Response) => {
res.send('Hello World from Express + TypeScript!');
});

// ... (其他路由 /users, /search 等) ...

// --- 404 Not Found 中间件 (应放在所有路由之后) ---
app.use((req: Request, res: Response, next: NextFunction) => {
res.status(404).send("Sorry, can't find that!");
});

// --- 错误处理中间件 (必须有 4 个参数: err, req, res, next) ---
// 应放在所有其他 app.use() 和路由之后
interface HttpError extends Error {
status?: number;
}

app.use((err: HttpError, req: Request, res: Response, next: NextFunction) => {
console.error(err.stack); // 打印错误堆栈信息
const statusCode = err.status || 500; // 获取错误状态码,默认为 500
res.status(statusCode).json({
message: err.message || 'Internal Server Error',
// 可以选择性地在开发环境中暴露堆栈信息
// stack: process.env.NODE_ENV === 'development' ? err.stack : undefined,
});
});

// 启动服务器
const port: number = parseInt(process.env.PORT || '3000', 10);
app.listen(port, () => {
console.log(Server is running at http://localhost:${port});
});

```

代码解析:

  1. requestLoggerMiddleware 函数:
    • 这是一个标准的中间件函数,接收 req, res, next 三个参数。
    • 我们为参数添加了类型注解 Request, Response, NextFunction
    • 它打印了请求的方法和原始 URL。
    • 关键: 调用 next() 将请求传递给处理链中的下一个函数。如果不调用 next() 并且不发送响应,请求将被挂起。
  2. app.use(requestLoggerMiddleware);
    • 使用 app.use() 将日志中间件注册为应用级中间件
    • 中间件的顺序很重要。 这个中间件放在所有路由之前,意味着每个进入应用的请求都会先经过这个日志记录器。
  3. 404 Not Found 中间件:
    • 这个中间件没有指定路径,这意味着它会匹配所有之前没有被任何路由处理的请求。
    • 它设置 404 状态码并发送一个 "Not Found" 消息。
    • 它必须放在所有正常路由定义的之后
  4. 错误处理中间件:
    • 特殊签名: 它有四个参数 (err, req, res, next)。Express 通过参数数量识别它是错误处理中间件。
    • 当在之前的路由处理器或中间件中调用 next(err) 时,或者当发生同步/异步错误(需要正确处理异步错误,例如使用 try-catch 或 Express 5 的内置支持)时,Express 会跳过所有普通中间件,直接将控制权交给第一个错误处理中间件。
    • 我们定义了一个 HttpError 接口继承自 Error,并添加了一个可选的 status 属性,方便传递 HTTP 状态码。
    • 它打印错误堆栈,然后根据错误对象中的 statusmessage(或提供默认值)向客户端发送一个 JSON 格式的错误响应。
    • 必须放在所有其他 app.use() 和路由定义的最后。

现在运行 npm run dev,每次你访问应用的不同路径时,都会在控制台看到类似 [2023-03-15T10:30:00.123Z] GET /users/123 的日志。如果访问一个不存在的路径(如 /nonexistent),你会收到 404 响应。如果某个路由内部发生错误(并被正确传递给 next(err)),则会触发错误处理中间件。

6.2 使用第三方中间件 (示例: CORS)

在开发 API 时,经常需要处理跨源资源共享 (CORS)。cors 是一个流行的 Node.js 中间件来处理这个问题。

安装 cors 及其类型定义:

bash
npm install cors
npm install --save-dev @types/cors

server.ts 中使用 cors:

```typescript
// src/server.ts
import express, { Request, Response, NextFunction, Application } from 'express';
import cors from 'cors'; // 导入 cors

// ... (其他 import) ...

const app: Application = express();

// --- 中间件 ---
// 启用 CORS (允许所有来源) - 应放在路由之前
app.use(cors());

// 日志中间件
const requestLoggerMiddleware = ... // (之前的定义)
app.use(requestLoggerMiddleware);

// 解析请求体
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// --- 路由定义 ---
// ... (之前的路由) ...

// --- 404 和错误处理中间件 ---
// ... (之前的定义) ...

// --- 启动服务器 ---
// ... (之前的定义) ...
```

通过简单地 app.use(cors()),你的 API 就允许来自任何源的跨域请求了。cors 中间件也支持更复杂的配置,例如只允许特定的源、方法或头部。


7. 模块化路由 (Express Router)

当应用变得复杂,路由越来越多时,将所有路由都定义在 server.ts 文件中会变得难以管理。Express 提供了 express.Router 来帮助我们将路由分割到不同的模块文件中。

7.1 创建路由文件

src 目录下创建一个新的 routes 文件夹,并在其中创建一个 userRoutes.ts 文件:

bash
mkdir src/routes
touch src/routes/userRoutes.ts

编写用户路由模块 (src/routes/userRoutes.ts):

```typescript
// src/routes/userRoutes.ts
import express, { Router, Request, Response } from 'express';

// 创建一个新的 Router 实例
const router: Router = express.Router();

// --- 类型定义 (可以放在单独的 types 文件中) ---
interface UserParams {
userId: string;
}

interface CreateUserPayload {
username: string;
email: string;
}

// --- 用户相关的路由 ---

// GET /users/:userId (注意:基础路径 /users 会在 server.ts 中设置)
router.get('/:userId', (req: Request, res: Response) => {
const userId: string = req.params.userId;
const id = parseInt(userId, 10);
if (isNaN(id)) {
return res.status(400).json({ message: 'Invalid user ID format' });
}
console.log('Fetching user with ID:', id);
const user = { id: id, username: user_${id}, email: user_${id}@example.com };
res.json(user);
});

// POST /users (注意:基础路径 /users 会在 server.ts 中设置)
router.post('/', (req: Request<{}, {}, CreateUserPayload>, res: Response) => {
const { username, email } = req.body;
if (!username || !email) {
return res.status(400).json({ message: 'Username and email are required' });
}
console.log('Creating user:', { username, email });
res.status(201).json({
message: 'User created successfully',
user: { id: Date.now(), username, email },
});
});

// 导出 Router 实例,以便在主应用中使用
export default router;
```

代码解析:

  1. import express, { Router, Request, Response } from 'express';: 导入 Router 类型。
  2. const router: Router = express.Router();: 创建一个 Router 对象。你可以把它看作是一个“迷你”的 Express 应用,可以像 app 一样在其上定义路由和中间件。
  3. router.get('/:userId', ...)router.post('/', ...): 在 router 实例上定义路由。注意路径的变化:
    • 原本的 app.get('/users/:userId', ...) 变成了 router.get('/:userId', ...)
    • 原本的 app.post('/users', ...) 变成了 router.post('/', ...)
    • 这是因为我们稍后会在主应用 (server.ts) 中将这个 router 挂载到 /users 路径下,所以这里定义的路径是相对于 /users 的。
  4. export default router;: 将配置好的 router 实例导出,以便其他文件可以导入和使用它。

7.2 在主应用中使用路由模块 (src/server.ts)

现在,修改 src/server.ts 来导入并使用 userRoutes

```typescript
// src/server.ts
import express, { Request, Response, NextFunction, Application } from 'express';
import cors from 'cors';
import userRoutes from './routes/userRoutes'; // 导入用户路由模块
// import searchRoutes from './routes/searchRoutes'; // 假设还有其他路由模块

// ... (其他 import 和 app 创建) ...

const app: Application = express();

// --- 中间件 ---
app.use(cors());
// ... (其他中间件: logger, body-parser) ...
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// --- 挂载路由模块 ---
// 将 userRoutes 挂载到 /users 路径下
app.use('/users', userRoutes);
// app.use('/search', searchRoutes); // 可以挂载更多路由

// --- 基本路由 (可以保留或也移到单独的路由文件) ---
app.get('/', (req: Request, res: Response) => {
res.send('Hello World from Express + TypeScript!');
});

// --- 404 和错误处理中间件 ---
// ... (之前的定义) ...

// --- 启动服务器 ---
// ... (之前的定义) ...
```

代码解析:

  1. import userRoutes from './routes/userRoutes';: 导入我们刚刚创建的用户路由模块。
  2. app.use('/users', userRoutes);: 使用 app.use()userRoutes 挂载到 /users 路径。这意味着:
    • userRoutes.ts 中定义的 GET /:userId 路由,现在对应的完整路径是 GET /users/:userId
    • userRoutes.ts 中定义的 POST / 路由,现在对应的完整路径是 POST /users
  3. 移除原来的用户路由: 记得从 server.ts 中删除原来直接写在 app 上的 /users/:userId/users 路由,避免重复定义。

通过这种方式,你可以为应用的不同功能(如用户、产品、订单等)创建各自的路由文件,使 server.ts 保持简洁,只负责全局配置、中间件加载和路由挂载。


8. 环境变量 (.env)

在开发和部署应用时,通常需要配置一些敏感信息(如数据库密码、API密钥)或环境特定的设置(如端口号、数据库地址)。将这些信息硬编码在代码中是不安全的,并且不利于在不同环境(开发、测试、生产)中部署。

推荐使用 .env 文件来管理环境变量。我们需要一个库 dotenv 来帮助加载 .env 文件中的变量到 Node.js 的 process.env 对象中。

安装 dotenv:

bash
npm install dotenv

创建 .env 文件:

在项目根目录下(与 package.json同级)创建一个名为 .env 的文件:

```plaintext

.env file

注释以 # 开头

PORT=4000
API_KEY=your_secret_api_key
DATABASE_URL=mongodb://localhost:27017/mydatabase
NODE_ENV=development
```

修改 server.ts 以加载 .env 文件:

server.ts最顶部(在所有其他代码之前)加载并配置 dotenv

```typescript
// src/server.ts
import dotenv from 'dotenv';
dotenv.config(); // 加载 .env 文件中的环境变量到 process.env

import express, { Request, Response, NextFunction, Application } from 'express';
import cors from 'cors';
import userRoutes from './routes/userRoutes';

// ... (app 创建) ...
const app: Application = express();

// --- 中间件 ---
// ...

// --- 路由 ---
// ...

// --- 错误处理 ---
// ...

// --- 启动服务器 ---
// 从 process.env 读取端口,如果未定义则使用默认值 3000
const port: number = parseInt(process.env.PORT || '3000', 10);

app.listen(port, () => {
console.log(Server environment: ${process.env.NODE_ENV}); // 可以访问其他环境变量
console.log(Server is running at http://localhost:${port});
// console.log(API Key: ${process.env.API_KEY}); // 不要在日志中打印敏感信息!
});
```

代码解析:

  1. import dotenv from 'dotenv';: 导入 dotenv 库。
  2. dotenv.config();: 调用 config() 方法。它会读取项目根目录下的 .env 文件,解析其中的键值对,并将它们添加到 process.env 对象上。这一行必须尽可能早地执行,以确保后续代码能访问到这些环境变量。
  3. process.env.PORT: 现在我们可以通过 process.env.VARIABLE_NAME 来访问 .env 文件中定义的变量了。我们将端口号的获取改为了优先从 process.env.PORT 读取。
  4. 访问其他变量: 你可以在代码的任何地方通过 process.env 访问 .env 文件中定义的其他变量,如 process.env.NODE_ENV, process.env.API_KEY 等。

重要:将 .env 文件添加到 .gitignore

.env 文件通常包含敏感信息,绝对不能提交到 Git 等版本控制系统中。确保在项目根目录的 .gitignore 文件中添加 .env

```plaintext

.gitignore

node_modules
dist
.env
*.log
```

在部署应用时,你需要在服务器上以安全的方式提供相应的环境变量(例如,通过服务器的环境变量设置、部署平台的配置、或专门的密钥管理服务)。


9. 构建生产版本

在开发过程中,我们使用 ts-nodenodemon 来方便地运行和调试 TypeScript 代码。但在部署到生产环境时,我们应该先将 TypeScript 代码编译成高效的、优化过的 JavaScript 代码,然后使用 Node.js 直接运行这些 JavaScript 文件。

编译 TypeScript 代码:

运行我们在 package.json 中定义的 build 脚本:

bash
npm run build

这个命令会执行 tsc。根据 tsconfig.json 的配置("outDir": "./dist"),TypeScript 编译器会将 src 目录下的所有 .ts 文件编译成 .js 文件,并输出到项目根目录下的 dist 文件夹中。

你会看到生成了一个 dist 目录,里面包含了编译后的 JavaScript 文件,保持了 src 目录的结构。例如,src/server.ts 会被编译成 dist/server.jssrc/routes/userRoutes.ts 会被编译成 dist/routes/userRoutes.js

运行生产版本:

编译完成后,使用 start 脚本来运行编译后的 JavaScript 代码:

bash
npm start

这个命令会执行 node dist/server.js(根据 package.jsonscripts.start 的定义)。Node.js 会直接运行 dist 目录下的 JavaScript 入口文件。

这种方式通常比在生产环境中使用 ts-node 更高效,并且避免了在生产服务器上安装 TypeScript 和其他开发依赖。

生产环境最佳实践提醒:

  • 确保只安装生产依赖 (npm install --productionnpm ci)。
  • 设置 NODE_ENV=production 环境变量,很多库(包括 Express)会根据这个变量进行性能优化,并可能禁用详细的错误信息。
  • 使用进程管理器(如 PM2、systemd)来管理 Node.js 应用,确保它能在后台运行、崩溃后自动重启、进行负载均衡等。
  • 配置更健壮的日志记录。
  • 考虑安全性措施(如使用 helmet 中间件设置安全的 HTTP 头)。

10. 总结与后续步骤

恭喜你!你已经学习了如何使用 Node.js、Express 和 TypeScript 从零开始搭建一个基础的 Web 应用/API 服务器。我们涵盖了:

  • 环境搭建和项目初始化 (npm, typescript, @types)
  • TypeScript 配置 (tsconfig.json)
  • 使用 TypeScript 编写 Express 服务器 (Application, Request, Response)
  • 处理不同的 HTTP 请求 (GET, POST)
  • 解析请求体 (express.json, express.urlencoded) 和使用类型接口 (interface)
  • 处理路由参数 (req.params) 和查询参数 (req.query)
  • 理解和使用中间件 (app.use, next, 自定义、第三方、错误处理)
  • 使用 express.Router 模块化路由
  • 使用 .envdotenv 管理环境变量
  • 编译 TypeScript 代码 (tsc) 并运行生产版本 (node)

这为你构建更复杂、更健壮的后端服务打下了坚实的基础。TypeScript 带来的类型安全和开发工具支持将在项目规模扩大时体现出巨大的价值。

下一步可以探索的方向:

  1. 数据库集成: 学习如何连接数据库(如 MongoDB 使用 Mongoose,PostgreSQL/MySQL 使用 TypeORM 或 Prisma)来持久化存储数据。
  2. 异步操作与 Promises/Async-Await: 深入理解 Node.js 的异步特性,并在 TypeScript 中熟练使用 async/await 处理数据库查询、API 调用等异步任务。
  3. 输入验证: 使用 zod, joi, class-validator 等库对请求数据(req.body, req.params, req.query)进行更严格和声明式的验证。
  4. 认证与授权: 实现用户登录、注册功能,使用 JWT (JSON Web Tokens) 或 Passport.js 等库保护你的 API 路由。
  5. 测试: 学习使用 Jest、Mocha、Chai、Supertest 等工具为你的 API 编写单元测试和集成测试。
  6. 更高级的 TypeScript 特性: 泛型、装饰器(在 TypeORM, class-validator 中常用)、命名空间等。
  7. 部署: 将你的应用部署到云平台(如 Heroku, AWS, Google Cloud, Vercel)或自己的服务器。
  8. WebSockets: 构建实时应用(如聊天室、实时通知)所需的双向通信。
  9. GraphQL: 探索另一种流行的 API 查询语言。

继续实践,不断学习,结合这三者的力量,你将能够构建出色的现代 Web 应用程序!祝你编码愉快!

THE END