Node.js后端开发入门教程

教程简介

零基础Node.js后端开发完整教程,涵盖模块系统、文件操作、HTTP服务、Express框架、REST API设计、数据库连接等核心技能,配有完整待办事项API实战项目,适合后端开发初学者。

Node.js 后端开发入门教程

📖 本教程面向零基础读者,从零开始讲解 Node.js 后端开发的核心知识。学完本教程,你将能够独立搭建一个完整的 RESTful API 服务。


目录

  1. Node.js 是什么
  2. 安装 Node.js
  3. 第一个 Node.js 程序
  4. 模块系统
  5. npm 包管理器
  6. 文件操作
  7. HTTP 服务基础
  8. Express 框架入门
  9. REST API 设计与实现
  10. 数据库连接
  11. 实战项目:搭建完整的 RESTful API
  12. 常见问题与调试技巧
  13. 下一步学习建议

1. Node.js 是什么

1.1 基本概念

Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境。简单来说,它让你可以在浏览器之外运行 JavaScript 代码。

在 Node.js 出现之前,JavaScript 只能在浏览器中运行,主要用于网页的交互效果。Node.js 的出现打破了这个限制,让 JavaScript 成为了一门可以编写服务端程序的语言。

1.2 为什么选择 Node.js

  • 语言统一:前后端都用 JavaScript,降低学习成本
  • 高性能:基于 V8 引擎,执行速度快
  • 事件驱动、非阻塞 I/O:非常适合处理高并发场景,如实时应用、API 服务
  • 丰富的生态系统:npm 拥有超过百万个开源包
  • 活跃的社区:遇到问题容易找到解决方案

1.3 Node.js 的适用场景

适用场景 说明
REST API 服务 最常见的用途,为前端或移动端提供数据接口
实时应用 聊天应用、协作工具、实时通知
微服务架构 轻量级服务,易于部署和扩展
命令行工具 构建开发工具、脚本自动化
爬虫程序 数据抓取和处理

1.4 Node.js 的架构

┌─────────────────────────────────────┐
│           你的 JavaScript 代码        │
├─────────────────────────────────────┤
│           Node.js 标准库             │
│    (fs, http, path, events...)      │
├─────────────────────────────────────┤
│           V8 引擎 (执行JS)           │
├─────────────────────────────────────┤
│       libuv (异步 I/O 处理)          │
├─────────────────────────────────────┤
│           操作系统                    │
└─────────────────────────────────────┘

Node.js 的核心是 事件循环(Event Loop) 机制。它通过单线程处理请求,但利用 libuv 库在底层进行异步 I/O 操作,从而实现高并发。


2. 安装 Node.js

2.1 下载安装

方式一:官网下载(推荐新手)

访问 Node.js 官网,下载 LTS(长期支持)版本,按提示安装即可。

方式二:使用 nvm 管理版本(推荐进阶用户)

# 安装 nvm(Node Version Manager)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash

# 重新加载 shell 配置
source ~/.bashrc

# 安装最新 LTS 版本
nvm install --lts

# 使用该版本
nvm use --lts

2.2 验证安装

# 查看 Node.js 版本
node --version
# 输出示例:v22.22.1

# 查看 npm 版本
npm --version
# 输出示例:10.9.0

如果两个命令都能正常输出版本号,说明安装成功。

2.3 了解 REPL 环境

Node.js 自带一个交互式命令行环境(REPL),可以直接在终端中执行 JavaScript 代码:

# 进入 REPL
node

# 试试这些命令
> 1 + 1
2
> console.log('Hello Node.js!')
Hello Node.js!
> process.version
'v22.22.1'

# 退出 REPL(按两次 Ctrl+C 或输入 .exit)
> .exit

REPL 非常适合快速测试代码片段和学习 JavaScript 语法。


3. 第一个 Node.js 程序

3.1 创建项目目录

mkdir my-first-node-app
cd my-first-node-app

3.2 编写代码

创建文件 hello.js

// hello.js
// 获取命令行参数(跳过前两个:node 和 文件路径)
const args = process.argv.slice(2);

if (args.length === 0) {
  console.log('你好!请告诉我你的名字:');
  console.log('用法:node hello.js <你的名字>');
} else {
  const name = args[0];
  console.log(`你好,${name}!欢迎学习 Node.js!`);
  console.log(`当前时间:${new Date().toLocaleString('zh-CN')}`);
  console.log(`Node.js 版本:${process.version}`);
  console.log(`运行平台:${process.platform}`);
}

3.3 运行程序

node hello.js
# 输出:你好!请告诉我你的名字...

node hello.js 小明
# 输出:你好,小明!欢迎学习 Node.js!
#       当前时间:2026/5/28 08:30:00
#       Node.js 版本:v22.22.1
#       运行平台:linux

3.4 初始化项目

每个正式的 Node.js 项目都应该通过 npm init 初始化:

npm init -y

这会生成 package.json 文件,它是项目的"身份证",记录了项目名称、版本、依赖等信息:

{
  "name": "my-first-node-app",
  "version": "1.0.0",
  "description": "",
  "main": "hello.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

你可以编辑 package.json,在 scripts 中添加自定义命令:

{
  "scripts": {
    "start": "node hello.js 小明",
    "dev": "node --watch hello.js 小明"
  }
}

然后用 npm startnpm run dev 来运行。--watch 参数会在文件修改后自动重启,方便开发调试。


4. 模块系统

模块系统是 Node.js 最重要的概念之一。它让你可以把代码拆分成多个文件,实现代码复用和组织管理。

4.1 CommonJS 模块(Node.js 默认)

导出模块 —— 创建 math.js

// math.js - 数学工具模块

// 方式一:逐个导出
function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

function multiply(a, b) {
  return a * b;
}

function divide(a, b) {
  if (b === 0) {
    throw new Error('除数不能为零');
  }
  return a / b;
}

// 导出函数
module.exports = { add, subtract, multiply, divide };

// 方式二(简写):直接在 exports 上挂载
// exports.add = add;
// exports.subtract = subtract;

导入模块 —— 创建 app.js

// app.js
const math = require('./math');

console.log(math.add(10, 5));       // 15
console.log(math.subtract(10, 5));  // 5
console.log(math.multiply(10, 5));  // 50
console.log(math.divide(10, 5));    // 2

// 也可以解构导入
const { add, subtract } = require('./math');
console.log(add(100, 200));  // 300

4.2 ES Module(现代写法)

从 Node.js 12 开始支持 ES Module 语法。需要在 package.json 中声明:

{
  "type": "module"
}

然后就可以使用 import/export 语法:

// math.mjs(或 .js + "type": "module")
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}
// app.mjs
import { add, subtract } from './math.mjs';
console.log(add(10, 5));  // 15

4.3 内置模块

Node.js 提供了大量内置模块,无需安装即可使用:

// 路径处理
const path = require('path');
console.log(path.join('/root', 'data', 'file.txt'));
// 输出:/root/data/file.txt

console.log(path.extname('photo.jpg'));
// 输出:.jpg

// 操作系统信息
const os = require('os');
console.log('操作系统:', os.type());
console.log('总内存:', (os.totalmem() / 1024 / 1024 / 1024).toFixed(2) + ' GB');
console.log('空闲内存:', (os.freemem() / 1024 / 1024 / 1024).toFixed(2) + ' GB');
console.log('CPU 核心数:', os.cpus().length);

// 事件系统
const EventEmitter = require('events');
const emitter = new EventEmitter();

// 监听事件
emitter.on('greet', (name) => {
  console.log(`你好,${name}!`);
});

// 触发事件
emitter.emit('greet', '小明');  // 输出:你好,小明!

// URL 解析
const url = require('url');
const parsed = new URL('https://example.com:3000/path?name=test&id=1');
console.log(parsed.hostname);  // example.com
console.log(parsed.port);      // 3000
console.log(parsed.pathname);  // /path
console.log(parsed.searchParams.get('name'));  // test

4.4 模块查找规则

当你写 require('express') 时,Node.js 按以下顺序查找:

  1. 核心模块:如 fshttppath 等,直接返回
  2. 相对/绝对路径:如 ./math/home/user/lib/utils
  3. node_modules 目录:从当前目录向上逐级查找 node_modules 文件夹
当前文件: /project/src/app.js
require('lodash') 的查找顺序:
  /project/src/node_modules/lodash
  /project/node_modules/lodash
  /node_modules/lodash

5. npm 包管理器

npm(Node Package Manager)是 Node.js 的包管理工具,用于安装和管理第三方库。

5.1 常用命令

# 初始化项目
npm init -y

# 安装包(添加到 dependencies)
npm install express
npm i express              # 简写

# 安装包(添加到 devDependencies,仅开发时使用)
npm install --save-dev nodemon
npm i -D nodemon           # 简写

# 安装指定版本
npm install express@4.18.2

# 全局安装
npm install -g nodemon

# 卸载包
npm uninstall express

# 更新包
npm update

# 查看已安装的包
npm list
npm list --depth=0         # 只看顶层依赖

# 运行脚本
npm start                  # 运行 "start" 脚本
npm run dev                # 运行 "dev" 脚本

5.2 package.json 详解

{
  "name": "my-api-server",
  "version": "1.0.0",
  "description": "一个简单的 API 服务器",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js",
    "test": "jest"
  },
  "dependencies": {
    "express": "^4.18.2",
    "mongoose": "^7.6.0"
  },
  "devDependencies": {
    "nodemon": "^3.0.0"
  },
  "engines": {
    "node": ">=18.0.0"
  }
}
  • dependencies:项目运行时需要的依赖
  • devDependencies:仅开发时需要的依赖(如测试框架、代码格式化工具)
  • scripts:自定义命令,用 npm run <命令名> 运行
  • engines:指定 Node.js 版本要求

5.3 package-lock.json

package-lock.json 锁定了每个依赖的精确版本,确保团队成员和部署环境使用完全相同的依赖版本。这个文件应该提交到版本控制中


6. 文件操作

文件操作是后端开发的基础能力。Node.js 的 fs(File System)模块提供了丰富的文件操作 API。

6.1 同步 vs 异步

Node.js 中的文件操作有两种模式:

  • 同步(Sync):阻塞执行,代码简单但会卡住主线程
  • 异步(Async):非阻塞执行,推荐在服务端使用
const fs = require('fs');

// ========== 同步方式(简单但会阻塞) ==========
try {
  const data = fs.readFileSync('./data.txt', 'utf-8');
  console.log('同步读取:', data);
} catch (err) {
  console.error('读取失败:', err.message);
}

// ========== 异步方式(推荐) ==========
fs.readFile('./data.txt', 'utf-8', (err, data) => {
  if (err) {
    console.error('读取失败:', err.message);
    return;
  }
  console.log('异步读取:', data);
});

// ========== Promise 方式(最推荐) ==========
const fsPromises = require('fs').promises;

async function readMyFile() {
  try {
    const data = await fsPromises.readFile('./data.txt', 'utf-8');
    console.log('Promise 读取:', data);
  } catch (err) {
    console.error('读取失败:', err.message);
  }
}

readMyFile();

6.2 文件读写

const fs = require('fs').promises;
const path = require('path');

async function fileOperations() {
  const filePath = path.join(__dirname, 'example.txt');

  // 写入文件(覆盖)
  await fs.writeFile(filePath, 'Hello, Node.js!\n这是第二行内容。', 'utf-8');
  console.log('文件写入成功');

  // 追加内容
  await fs.appendFile(filePath, '\n这是追加的内容。', 'utf-8');
  console.log('内容追加成功');

  // 读取文件
  const content = await fs.readFile(filePath, 'utf-8');
  console.log('文件内容:\n', content);

  // 检查文件是否存在
  try {
    await fs.access(filePath);
    console.log('文件存在');
  } catch {
    console.log('文件不存在');
  }

  // 获取文件信息
  const stats = await fs.stat(filePath);
  console.log('文件大小:', stats.size, '字节');
  console.log('修改时间:', stats.mtime);

  // 删除文件
  // await fs.unlink(filePath);
  // console.log('文件已删除');
}

fileOperations().catch(console.error);

6.3 目录操作

const fs = require('fs').promises;
const path = require('path');

async function dirOperations() {
  const dirPath = path.join(__dirname, 'my-folder');
  const subDir = path.join(dirPath, 'sub-folder');

  // 创建目录(recursive: true 允许递归创建多层目录)
  await fs.mkdir(subDir, { recursive: true });
  console.log('目录创建成功');

  // 在目录中创建几个文件
  await fs.writeFile(path.join(dirPath, 'file1.txt'), '文件1的内容');
  await fs.writeFile(path.join(dirPath, 'file2.json'), '{"name": "test"}');
  await fs.writeFile(path.join(subDir, 'file3.txt'), '文件3的内容');

  // 读取目录内容
  const files = await fs.readdir(dirPath);
  console.log('目录内容:', files);
  // 输出:['file1.txt', 'file2.json', 'sub-folder']

  // 读取目录内容(带详细信息)
  const filesWithTypes = await fs.readdir(dirPath, { withFileTypes: true });
  for (const file of filesWithTypes) {
    const type = file.isDirectory() ? '📁 目录' : '📄 文件';
    console.log(`  ${type}: ${file.name}`);
  }

  // 递归列出所有文件
  async function listAllFiles(dir, prefix = '') {
    const entries = await fs.readdir(dir, { withFileTypes: true });
    for (const entry of entries) {
      const fullPath = path.join(dir, entry.name);
      if (entry.isDirectory()) {
        console.log(`${prefix}📁 ${entry.name}/`);
        await listAllFiles(fullPath, prefix + '  ');
      } else {
        console.log(`${prefix}📄 ${entry.name}`);
      }
    }
  }

  console.log('\n完整目录结构:');
  await listAllFiles(dirPath);
}

dirOperations().catch(console.error);

6.4 JSON 文件操作

后端开发中经常需要读写 JSON 配置文件或数据文件:

const fs = require('fs').promises;
const path = require('path');

const configPath = path.join(__dirname, 'config.json');

// 读取 JSON 文件
async function readJSON(filePath) {
  const data = await fs.readFile(filePath, 'utf-8');
  return JSON.parse(data);
}

// 写入 JSON 文件(格式化输出)
async function writeJSON(filePath, data) {
  await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');
}

// 使用示例
async function main() {
  // 创建初始配置
  const config = {
    app: { name: 'MyApp', version: '1.0.0' },
    server: { port: 3000, host: 'localhost' },
    database: { url: 'mongodb://localhost:27017/mydb' }
  };
  await writeJSON(configPath, config);
  console.log('配置文件已创建');

  // 读取并修改配置
  const loaded = await readJSON(configPath);
  loaded.server.port = 8080;
  await writeJSON(configPath, loaded);
  console.log('配置文件已更新');
}

main().catch(console.error);

7. HTTP 服务基础

Node.js 内置了 http 模块,无需任何第三方依赖就能创建 Web 服务器。

7.1 最简单的 HTTP 服务器

const http = require('http');

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
  res.end('Hello, World!\n');
});

server.listen(3000, () => {
  console.log('服务器运行在 http://localhost:3000');
});

运行后,在浏览器访问 http://localhost:3000 就能看到页面。

7.2 处理不同路由

const http = require('http');

const server = http.createServer((req, res) => {
  const { method, url } = req;

  // 设置响应头
  res.setHeader('Content-Type', 'application/json; charset=utf-8');

  if (method === 'GET' && url === '/') {
    res.writeHead(200);
    res.end(JSON.stringify({ message: '欢迎访问首页' }));
  } else if (method === 'GET' && url === '/about') {
    res.writeHead(200);
    res.end(JSON.stringify({ message: '关于我们' }));
  } else if (method === 'GET' && url === '/api/users') {
    res.writeHead(200);
    res.end(JSON.stringify({
      users: [
        { id: 1, name: '张三' },
        { id: 2, name: '李四' }
      ]
    }));
  } else {
    res.writeHead(404);
    res.end(JSON.stringify({ error: '页面未找到' }));
  }
});

server.listen(3000, () => {
  console.log('服务器运行在 http://localhost:3000');
});

7.3 解析请求体

当客户端发送 POST 请求时,需要读取请求体数据:

const http = require('http');

const server = http.createServer((req, res) => {
  res.setHeader('Content-Type', 'application/json; charset=utf-8');

  if (req.method === 'POST' && req.url === '/api/data') {
    let body = '';

    // 监听数据块
    req.on('data', (chunk) => {
      body += chunk.toString();
    });

    // 数据接收完毕
    req.on('end', () => {
      try {
        const parsed = JSON.parse(body);
        console.log('收到数据:', parsed);

        res.writeHead(200);
        res.end(JSON.stringify({
          success: true,
          received: parsed
        }));
      } catch (err) {
        res.writeHead(400);
        res.end(JSON.stringify({ error: '无效的 JSON 数据' }));
      }
    });
  } else {
    res.writeHead(404);
    res.end(JSON.stringify({ error: '未找到' }));
  }
});

server.listen(3000, () => {
  console.log('服务器运行在 http://localhost:3000');
});

7.4 原生 HTTP 的局限

虽然原生 http 模块能用,但手动处理路由、请求体解析、错误处理等非常繁琐。这就是为什么实际开发中我们会使用 Express 框架。


8. Express 框架入门

Express 是 Node.js 最流行的 Web 框架,简洁灵活,是大多数 Node.js 项目的首选。

8.1 安装 Express

npm install express

8.2 基本用法

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

// 中间件:解析 JSON 请求体
app.use(express.json());

// 中间件:解析 URL 编码的请求体
app.use(express.urlencoded({ extended: true }));

// 路由
app.get('/', (req, res) => {
  res.json({ message: 'Hello Express!' });
});

app.get('/about', (req, res) => {
  res.json({ message: '关于我们' });
});

// 启动服务器
app.listen(PORT, () => {
  console.log(`服务器运行在 http://localhost:${PORT}`);
});

8.3 路由详解

const express = require('express');
const app = express();

app.use(express.json());

// ========== 基础路由 ==========

// GET 请求
app.get('/api/products', (req, res) => {
  res.json({ products: ['手机', '电脑', '平板'] });
});

// POST 请求
app.post('/api/products', (req, res) => {
  const { name, price } = req.body;
  res.status(201).json({ message: `商品 ${name} 创建成功`, price });
});

// PUT 请求
app.put('/api/products/:id', (req, res) => {
  const { id } = req.params;
  const { name, price } = req.body;
  res.json({ message: `商品 ${id} 更新成功`, name, price });
});

// DELETE 请求
app.delete('/api/products/:id', (req, res) => {
  const { id } = req.params;
  res.json({ message: `商品 ${id} 删除成功` });
});

// ========== 路由参数 ==========

app.get('/api/users/:id', (req, res) => {
  const { id } = req.params;
  res.json({ userId: id });
});

// 多个参数
app.get('/api/posts/:postId/comments/:commentId', (req, res) => {
  const { postId, commentId } = req.params;
  res.json({ postId, commentId });
});

// ========== 查询参数 ==========

// GET /api/search?q=nodejs&page=1&limit=10
app.get('/api/search', (req, res) => {
  const { q, page = 1, limit = 10 } = req.query;
  res.json({
    query: q,
    page: Number(page),
    limit: Number(limit)
  });
});

app.listen(3000, () => {
  console.log('服务器运行在 http://localhost:3000');
});

8.4 中间件

中间件是 Express 的核心概念。每个中间件函数可以访问请求对象(req)、响应对象(res)和下一个中间件函数(next)。

const express = require('express');
const app = express();

app.use(express.json());

// ========== 自定义中间件 ==========

// 请求日志中间件
function logger(req, res, next) {
  const start = Date.now();
  const { method, url } = req;

  // 在响应结束时记录耗时
  res.on('finish', () => {
    const duration = Date.now() - start;
    console.log(`${method} ${url} ${res.statusCode} - ${duration}ms`);
  });

  next();  // 调用下一个中间件
}

// 认证中间件
function authenticate(req, res, next) {
  const token = req.headers.authorization;

  if (!token || token !== 'Bearer my-secret-token') {
    return res.status(401).json({ error: '未授权,请提供有效的 token' });
  }

  // 将用户信息挂载到 req 上
  req.user = { id: 1, name: '张三' };
  next();
}

// 使用全局中间件
app.use(logger);

// 公开路由(不需要认证)
app.get('/api/public', (req, res) => {
  res.json({ message: '这是公开数据' });
});

// 受保护的路由(需要认证)
app.get('/api/profile', authenticate, (req, res) => {
  res.json({ user: req.user });
});

// 也可以对一组路由使用中间件
app.use('/api/admin', authenticate);
app.get('/api/admin/dashboard', (req, res) => {
  res.json({ message: '管理后台数据' });
});

app.listen(3000, () => {
  console.log('服务器运行在 http://localhost:3000');
});

8.5 错误处理

const express = require('express');
const app = express();

app.use(express.json());

app.get('/api/users/:id', async (req, res, next) => {
  try {
    const { id } = req.params;

    // 模拟数据库查询
    if (id === '999') {
      throw new Error('用户不存在');
    }

    res.json({ id, name: '张三' });
  } catch (err) {
    next(err);  // 将错误传递给错误处理中间件
  }
});

// 错误处理中间件(必须有 4 个参数)
app.use((err, req, res, next) => {
  console.error('错误:', err.message);
  res.status(500).json({
    error: '服务器内部错误',
    message: err.message
  });
});

app.listen(3000, () => {
  console.log('服务器运行在 http://localhost:3000');
});

9. REST API 设计与实现

9.1 REST 基本概念

REST(Representational State Transfer)是一种 API 设计风格。遵循 REST 规范的 API 叫 RESTful API。

核心原则:

  • 资源导向:每个 URL 代表一个资源
  • 使用 HTTP 方法:GET(查询)、POST(创建)、PUT(更新)、DELETE(删除)
  • 无状态:每个请求包含所有必要信息
  • 统一的响应格式

9.2 设计规范

操作 HTTP 方法 URL 说明
获取列表 GET /api/users 获取所有用户
获取单个 GET /api/users/:id 获取指定用户
创建 POST /api/users 创建新用户
更新 PUT /api/users/:id 更新指定用户
删除 DELETE /api/users/:id 删除指定用户

9.3 完整的 REST API 示例

const express = require('express');
const app = express();

app.use(express.json());

// 模拟数据库(用内存中的数组代替)
let users = [
  { id: 1, name: '张三', email: 'zhangsan@example.com', age: 28 },
  { id: 2, name: '李四', email: 'lisi@example.com', age: 32 },
  { id: 3, name: '王五', email: 'wangwu@example.com', age: 25 }
];

let nextId = 4;

// ========== 工具函数 ==========

function findUserById(id) {
  return users.find(u => u.id === Number(id));
}

function validateUser(data) {
  const errors = [];
  if (!data.name || data.name.trim() === '') {
    errors.push('姓名不能为空');
  }
  if (!data.email || !data.email.includes('@')) {
    errors.push('请提供有效的邮箱地址');
  }
  if (data.age !== undefined && (data.age < 0 || data.age > 150)) {
    errors.push('年龄必须在 0-150 之间');
  }
  return errors;
}

// ========== 路由 ==========

// GET /api/users - 获取用户列表(支持分页和筛选)
app.get('/api/users', (req, res) => {
  let { page = 1, limit = 10, name, minAge, maxAge } = req.query;
  page = Math.max(1, Number(page));
  limit = Math.min(100, Math.max(1, Number(limit)));

  let filtered = [...users];

  // 按姓名筛选
  if (name) {
    filtered = filtered.filter(u =>
      u.name.includes(name)
    );
  }

  // 按年龄范围筛选
  if (minAge) {
    filtered = filtered.filter(u => u.age >= Number(minAge));
  }
  if (maxAge) {
    filtered = filtered.filter(u => u.age <= Number(maxAge));
  }

  // 分页
  const total = filtered.length;
  const start = (page - 1) * limit;
  const paginated = filtered.slice(start, start + limit);

  res.json({
    success: true,
    data: paginated,
    pagination: {
      page,
      limit,
      total,
      totalPages: Math.ceil(total / limit)
    }
  });
});

// GET /api/users/:id - 获取单个用户
app.get('/api/users/:id', (req, res) => {
  const user = findUserById(req.params.id);

  if (!user) {
    return res.status(404).json({
      success: false,
      error: '用户不存在'
    });
  }

  res.json({ success: true, data: user });
});

// POST /api/users - 创建用户
app.post('/api/users', (req, res) => {
  const errors = validateUser(req.body);
  if (errors.length > 0) {
    return res.status(400).json({ success: false, errors });
  }

  // 检查邮箱是否已存在
  if (users.some(u => u.email === req.body.email)) {
    return res.status(409).json({
      success: false,
      error: '该邮箱已被注册'
    });
  }

  const newUser = {
    id: nextId++,
    name: req.body.name.trim(),
    email: req.body.email.trim(),
    age: req.body.age || null
  };

  users.push(newUser);

  res.status(201).json({ success: true, data: newUser });
});

// PUT /api/users/:id - 更新用户(全量更新)
app.put('/api/users/:id', (req, res) => {
  const user = findUserById(req.params.id);

  if (!user) {
    return res.status(404).json({
      success: false,
      error: '用户不存在'
    });
  }

  const errors = validateUser(req.body);
  if (errors.length > 0) {
    return res.status(400).json({ success: false, errors });
  }

  user.name = req.body.name.trim();
  user.email = req.body.email.trim();
  user.age = req.body.age || null;

  res.json({ success: true, data: user });
});

// PATCH /api/users/:id - 部分更新
app.patch('/api/users/:id', (req, res) => {
  const user = findUserById(req.params.id);

  if (!user) {
    return res.status(404).json({
      success: false,
      error: '用户不存在'
    });
  }

  // 只更新传入的字段
  if (req.body.name !== undefined) user.name = req.body.name.trim();
  if (req.body.email !== undefined) user.email = req.body.email.trim();
  if (req.body.age !== undefined) user.age = req.body.age;

  res.json({ success: true, data: user });
});

// DELETE /api/users/:id - 删除用户
app.delete('/api/users/:id', (req, res) => {
  const index = users.findIndex(u => u.id === Number(req.params.id));

  if (index === -1) {
    return res.status(404).json({
      success: false,
      error: '用户不存在'
    });
  }

  const deleted = users.splice(index, 1)[0];
  res.json({ success: true, data: deleted });
});

// ========== 404 处理 ==========

app.use((req, res) => {
  res.status(404).json({ error: `未找到路由: ${req.method} ${req.url}` });
});

// ========== 错误处理 ==========

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: '服务器内部错误' });
});

// ========== 启动服务器 ==========

app.listen(3000, () => {
  console.log('API 服务器运行在 http://localhost:3000');
  console.log('试试这些端点:');
  console.log('  GET    /api/users');
  console.log('  GET    /api/users/1');
  console.log('  POST   /api/users');
  console.log('  PUT    /api/users/1');
  console.log('  DELETE /api/users/1');
});

10. 数据库连接

实际项目中,数据需要持久化存储。这里介绍两种常用数据库的连接方式。

10.1 MongoDB + Mongoose

MongoDB 是一个 NoSQL 文档数据库,适合存储 JSON 格式的数据。Mongoose 是 MongoDB 的 ODM(对象文档映射)库,提供了数据验证、类型转换等功能。

安装:

npm install mongoose

连接与使用:

const mongoose = require('mongoose');

// 连接数据库
async function connectDB() {
  try {
    await mongoose.connect('mongodb://localhost:27017/myapp');
    console.log('MongoDB 连接成功');
  } catch (err) {
    console.error('MongoDB 连接失败:', err.message);
    process.exit(1);
  }
}

// 定义数据模型(Schema)
const userSchema = new mongoose.Schema({
  name: {
    type: String,
    required: [true, '姓名是必填项'],
    trim: true,
    maxlength: [50, '姓名不能超过50个字符']
  },
  email: {
    type: String,
    required: [true, '邮箱是必填项'],
    unique: true,
    lowercase: true,
    match: [/^\S+@\S+\.\S+$/, '请提供有效的邮箱地址']
  },
  age: {
    type: Number,
    min: [0, '年龄不能为负数'],
    max: [150, '年龄不能超过150']
  },
  role: {
    type: String,
    enum: ['user', 'admin'],
    default: 'user'
  },
  createdAt: {
    type: Date,
    default: Date.now
  }
});

// 添加虚拟字段
userSchema.virtual('displayName').get(function () {
  return `${this.name} (${this.email})`;
});

// 添加实例方法
userSchema.methods.toSafeJSON = function () {
  const obj = this.toObject();
  delete obj.__v;
  return obj;
};

// 添加静态方法
userSchema.statics.findByEmail = function (email) {
  return this.findOne({ email: email.toLowerCase() });
};

// 创建模型
const User = mongoose.model('User', userSchema);

// ========== CRUD 操作示例 ==========

async function main() {
  await connectDB();

  // 创建用户
  const user = await User.create({
    name: '张三',
    email: 'zhangsan@example.com',
    age: 28
  });
  console.log('创建用户:', user.toSafeJSON());

  // 查询用户
  const found = await User.findByEmail('zhangsan@example.com');
  console.log('查询用户:', found.displayName);

  // 更新用户
  const updated = await User.findByIdAndUpdate(
    user._id,
    { age: 29 },
    { new: true, runValidators: true }
  );
  console.log('更新后:', updated.toSafeJSON());

  // 查询多个
  const allUsers = await User.find({ role: 'user' }).sort({ createdAt: -1 });
  console.log('所有用户:', allUsers.length, '个');

  // 删除用户
  await User.findByIdAndDelete(user._id);
  console.log('用户已删除');

  // 断开连接
  await mongoose.disconnect();
}

main().catch(console.error);

10.2 MySQL + mysql2

MySQL 是最流行的关系型数据库之一。mysql2 是一个高性能的 MySQL 客户端库。

安装:

npm install mysql2

连接与使用:

const mysql = require('mysql2/promise');

// 创建连接池
const pool = mysql.createPool({
  host: 'localhost',
  user: 'root',
  password: 'your_password',
  database: 'myapp',
  waitForConnections: true,
  connectionLimit: 10,
  queueLimit: 0
});

// ========== 初始化表 ==========

async function initDB() {
  const connection = await pool.getConnection();
  try {
    await connection.execute(`
      CREATE TABLE IF NOT EXISTS users (
        id INT AUTO_INCREMENT PRIMARY KEY,
        name VARCHAR(100) NOT NULL,
        email VARCHAR(255) NOT NULL UNIQUE,
        age INT,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
      )
    `);
    console.log('表初始化成功');
  } finally {
    connection.release();
  }
}

// ========== CRUD 操作 ==========

// 创建
async function createUser(name, email, age) {
  const [result] = await pool.execute(
    'INSERT INTO users (name, email, age) VALUES (?, ?, ?)',
    [name, email, age]
  );
  return { id: result.insertId, name, email, age };
}

// 查询所有
async function getAllUsers() {
  const [rows] = await pool.execute('SELECT * FROM users ORDER BY created_at DESC');
  return rows;
}

// 查询单个
async function getUserById(id) {
  const [rows] = await pool.execute('SELECT * FROM users WHERE id = ?', [id]);
  return rows[0] || null;
}

// 更新
async function updateUser(id, data) {
  const fields = [];
  const values = [];

  if (data.name !== undefined) { fields.push('name = ?'); values.push(data.name); }
  if (data.email !== undefined) { fields.push('email = ?'); values.push(data.email); }
  if (data.age !== undefined) { fields.push('age = ?'); values.push(data.age); }

  if (fields.length === 0) return null;

  values.push(id);
  await pool.execute(`UPDATE users SET ${fields.join(', ')} WHERE id = ?`, values);
  return getUserById(id);
}

// 删除
async function deleteUser(id) {
  const user = await getUserById(id);
  if (!user) return null;
  await pool.execute('DELETE FROM users WHERE id = ?', [id]);
  return user;
}

// ========== 主程序 ==========

async function main() {
  await initDB();

  const user = await createUser('张三', 'zhangsan@example.com', 28);
  console.log('创建用户:', user);

  const all = await getAllUsers();
  console.log('所有用户:', all);

  await pool.end();
}

main().catch(console.error);

10.3 SQLite(轻量级选择)

如果项目不需要独立的数据库服务器,SQLite 是一个很好的选择。它将数据存储在单个文件中。

npm install better-sqlite3
const Database = require('better-sqlite3');
const path = require('path');

// 创建/打开数据库(会在当前目录生成 data.db 文件)
const db = new Database(path.join(__dirname, 'data.db'));

// 启用 WAL 模式(提升并发性能)
db.pragma('journal_mode = WAL');

// 创建表
db.exec(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT NOT NULL UNIQUE,
    age INTEGER,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
  )
`);

// 预编译 SQL 语句(提升性能)
const insertUser = db.prepare('INSERT INTO users (name, email, age) VALUES (?, ?, ?)');
const findUser = db.prepare('SELECT * FROM users WHERE id = ?');
const allUsers = db.prepare('SELECT * FROM users');
const removeUser = db.prepare('DELETE FROM users WHERE id = ?');

// 创建
const result = insertUser.run('张三', 'zhangsan@example.com', 28);
console.log('创建成功,ID:', result.lastInsertRowid);

// 查询
const user = findUser.get(1);
console.log('查询结果:', user);

// 查询所有
const users = allUsers.all();
console.log('所有用户:', users);

// 删除
removeUser.run(1);

// 关闭数据库
db.close();

11. 实战项目:搭建完整的 RESTful API

现在让我们把前面学到的知识整合起来,搭建一个完整的「待办事项 API」。

11.1 项目结构

todo-api/
├── package.json
├── src/
│   ├── index.js          # 入口文件
│   ├── routes/
│   │   └── todos.js      # 路由定义
│   ├── middleware/
│   │   ├── logger.js     # 日志中间件
│   │   └── errorHandler.js # 错误处理
│   └── data/
│       └── todos.json    # 数据存储(模拟数据库)
└── README.md

11.2 初始化项目

mkdir todo-api
cd todo-api
npm init -y
npm install express
npm install -D nodemon

编辑 package.json

{
  "name": "todo-api",
  "version": "1.0.0",
  "scripts": {
    "start": "node src/index.js",
    "dev": "nodemon src/index.js"
  }
}

11.3 完整代码

src/index.js —— 入口文件:

const express = require('express');
const path = require('path');
const todoRoutes = require('./routes/todos');
const logger = require('./middleware/logger');
const errorHandler = require('./middleware/errorHandler');

const app = express();
const PORT = process.env.PORT || 3000;

// ========== 中间件 ==========
app.use(express.json());
app.use(logger);

// ========== 路由 ==========
app.get('/', (req, res) => {
  res.json({
    message: '待办事项 API',
    version: '1.0.0',
    endpoints: {
      'GET /api/todos': '获取所有待办',
      'GET /api/todos/:id': '获取单个待办',
      'POST /api/todos': '创建待办',
      'PUT /api/todos/:id': '更新待办',
      'PATCH /api/todos/:id/toggle': '切换完成状态',
      'DELETE /api/todos/:id': '删除待办',
      'GET /api/stats': '获取统计信息'
    }
  });
});

app.use('/api/todos', todoRoutes);

// ========== 错误处理 ==========
app.use((req, res) => {
  res.status(404).json({ error: `未找到路由: ${req.method} ${req.url}` });
});
app.use(errorHandler);

// ========== 启动 ==========
app.listen(PORT, () => {
  console.log(`✅ 待办事项 API 运行在 http://localhost:${PORT}`);
  console.log(`📝 环境: ${process.env.NODE_ENV || 'development'}`);
});

src/middleware/logger.js —— 日志中间件:

function logger(req, res, next) {
  const start = Date.now();
  const { method, url } = req;

  res.on('finish', () => {
    const duration = Date.now() - start;
    const status = res.statusCode;
    const statusEmoji = status >= 400 ? '❌' : '✅';
    console.log(`${statusEmoji} ${method} ${url} ${status} - ${duration}ms`);
  });

  next();
}

module.exports = logger;

src/middleware/errorHandler.js —— 错误处理中间件:

function errorHandler(err, req, res, next) {
  console.error('🔥 错误:', err.message);
  console.error(err.stack);

  res.status(err.status || 500).json({
    error: err.message || '服务器内部错误',
    ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
  });
}

module.exports = errorHandler;

src/routes/todos.js —— 待办事项路由:

const express = require('express');
const router = express.Router();
const fs = require('fs').promises;
const path = require('path');

const DATA_FILE = path.join(__dirname, '..', 'data', 'todos.json');

// ========== 数据操作工具 ==========

async function readTodos() {
  try {
    const data = await fs.readFile(DATA_FILE, 'utf-8');
    return JSON.parse(data);
  } catch {
    return [];
  }
}

async function writeTodos(todos) {
  await fs.mkdir(path.dirname(DATA_FILE), { recursive: true });
  await fs.writeFile(DATA_FILE, JSON.stringify(todos, null, 2), 'utf-8');
}

function validateTodo(data) {
  const errors = [];
  if (!data.title || data.title.trim() === '') {
    errors.push('标题不能为空');
  }
  if (data.title && data.title.length > 200) {
    errors.push('标题不能超过200个字符');
  }
  if (data.priority && !['low', 'medium', 'high'].includes(data.priority)) {
    errors.push('优先级必须是 low、medium 或 high');
  }
  return errors;
}

// ========== 路由 ==========

// GET /api/todos - 获取待办列表(支持筛选和分页)
router.get('/', async (req, res) => {
  const { completed, priority, page = 1, limit = 20 } = req.query;
  let todos = await readTodos();

  // 筛选
  if (completed !== undefined) {
    todos = todos.filter(t => t.completed === (completed === 'true'));
  }
  if (priority) {
    todos = todos.filter(t => t.priority === priority);
  }

  // 按创建时间倒序
  todos.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));

  // 分页
  const total = todos.length;
  const start = (Number(page) - 1) * Number(limit);
  const paginated = todos.slice(start, start + Number(limit));

  res.json({
    success: true,
    data: paginated,
    pagination: {
      page: Number(page),
      limit: Number(limit),
      total,
      totalPages: Math.ceil(total / Number(limit))
    }
  });
});

// GET /api/stats - 获取统计信息
router.get('/stats', async (req, res) => {
  const todos = await readTodos();
  const total = todos.length;
  const completed = todos.filter(t => t.completed).length;
  const pending = total - completed;
  const byPriority = {
    high: todos.filter(t => t.priority === 'high').length,
    medium: todos.filter(t => t.priority === 'medium').length,
    low: todos.filter(t => t.priority === 'low').length
  };

  res.json({
    success: true,
    data: { total, completed, pending, byPriority }
  });
});

// GET /api/todos/:id - 获取单个待办
router.get('/:id', async (req, res) => {
  const todos = await readTodos();
  const todo = todos.find(t => t.id === req.params.id);

  if (!todo) {
    return res.status(404).json({ success: false, error: '待办事项不存在' });
  }

  res.json({ success: true, data: todo });
});

// POST /api/todos - 创建待办
router.post('/', async (req, res) => {
  const errors = validateTodo(req.body);
  if (errors.length > 0) {
    return res.status(400).json({ success: false, errors });
  }

  const todos = await readTodos();

  const newTodo = {
    id: Date.now().toString(36) + Math.random().toString(36).slice(2, 7),
    title: req.body.title.trim(),
    description: req.body.description?.trim() || '',
    completed: false,
    priority: req.body.priority || 'medium',
    createdAt: new Date().toISOString(),
    updatedAt: new Date().toISOString()
  };

  todos.push(newTodo);
  await writeTodos(todos);

  res.status(201).json({ success: true, data: newTodo });
});

// PUT /api/todos/:id - 更新待办
router.put('/:id', async (req, res) => {
  const errors = validateTodo(req.body);
  if (errors.length > 0) {
    return res.status(400).json({ success: false, errors });
  }

  const todos = await readTodos();
  const index = todos.findIndex(t => t.id === req.params.id);

  if (index === -1) {
    return res.status(404).json({ success: false, error: '待办事项不存在' });
  }

  todos[index] = {
    ...todos[index],
    title: req.body.title.trim(),
    description: req.body.description?.trim() || todos[index].description,
    priority: req.body.priority || todos[index].priority,
    updatedAt: new Date().toISOString()
  };

  await writeTodos(todos);
  res.json({ success: true, data: todos[index] });
});

// PATCH /api/todos/:id/toggle - 切换完成状态
router.patch('/:id/toggle', async (req, res) => {
  const todos = await readTodos();
  const todo = todos.find(t => t.id === req.params.id);

  if (!todo) {
    return res.status(404).json({ success: false, error: '待办事项不存在' });
  }

  todo.completed = !todo.completed;
  todo.updatedAt = new Date().toISOString();

  await writeTodos(todos);
  res.json({ success: true, data: todo });
});

// DELETE /api/todos/:id - 删除待办
router.delete('/:id', async (req, res) => {
  const todos = await readTodos();
  const index = todos.findIndex(t => t.id === req.params.id);

  if (index === -1) {
    return res.status(404).json({ success: false, error: '待办事项不存在' });
  }

  const deleted = todos.splice(index, 1)[0];
  await writeTodos(todos);
  res.json({ success: true, data: deleted });
});

module.exports = router;

11.4 测试 API

启动服务器:

npm run dev

使用 curl 测试各个接口:

# 创建待办
curl -X POST http://localhost:3000/api/todos \
  -H "Content-Type: application/json" \
  -d '{"title": "学习 Node.js", "priority": "high"}'

curl -X POST http://localhost:3000/api/todos \
  -H "Content-Type: application/json" \
  -d '{"title": "写单元测试", "priority": "medium"}'

# 获取所有待办
curl http://localhost:3000/api/todos

# 获取单个待办(替换为实际 ID)
curl http://localhost:3000/api/todos/<id>

# 切换完成状态
curl -X PATCH http://localhost:3000/api/todos/<id>/toggle

# 筛选未完成的待办
curl "http://localhost:3000/api/todos?completed=false"

# 获取统计信息
curl http://localhost:3000/api/stats

# 删除待办
curl -X DELETE http://localhost:3000/api/todos/<id>

12. 常见问题与调试技巧

12.1 常见错误

端口被占用:

# 查看占用端口的进程
lsof -i :3000
# 或
netstat -tlnp | grep 3000

# 杀掉进程
kill -9 <PID>

模块找不到:

# 重新安装依赖
rm -rf node_modules package-lock.json
npm install

异步错误未捕获:

// ❌ 错误写法:异步错误不会被 Express 捕获
app.get('/api/data', (req, res) => {
  const data = await someAsyncFunction(); // 报错!
  res.json(data);
});

// ✅ 正确写法:使用 try-catch
app.get('/api/data', async (req, res) => {
  try {
    const data = await someAsyncFunction();
    res.json(data);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// ✅ 或使用 express-async-errors 包(自动捕获)
// npm install express-async-errors
require('express-async-errors');

12.2 调试技巧

使用 console 调试:

// 基本输出
console.log('变量值:', someVar);

// 格式化输出
console.log(JSON.stringify(complexObject, null, 2));

// 带标签的输出
console.log('[DB]', '连接成功');
console.log('[API]', '请求处理完成');

// 计时
console.time('数据库查询');
// ... 某些操作
console.timeEnd('数据库查询');  // 输出:数据库查询: 15.326ms

// 错误输出(带堆栈信息)
console.error('错误详情:', error);

使用 Node.js 内置调试器:

# 以调试模式启动
node --inspect src/index.js

# 然后在 Chrome 浏览器中打开 chrome://inspect
# 可以设置断点、查看变量、单步执行

使用环境变量区分环境:

// 根据环境切换行为
const isDev = process.env.NODE_ENV !== 'production';

if (isDev) {
  console.log('调试信息...');
}

// 启动时指定环境
// NODE_ENV=production node src/index.js

13. 下一步学习建议

恭喜你完成了 Node.js 后端开发的入门学习!以下是一些进阶方向:

13.1 进阶主题

  • 认证与授权:JWT(JSON Web Token)、OAuth 2.0、Session
  • 数据验证:使用 Joi 或 Zod 进行请求数据校验
  • API 文档:使用 Swagger/OpenAPI 自动生成接口文档
  • 测试:Jest(单元测试)、Supertest(接口测试)
  • 日志:Winston 或 Pino(结构化日志)
  • 安全:CORS、Rate Limiting、Helmet、输入过滤
  • 部署:Docker 容器化、PM2 进程管理、Nginx 反向代理

13.2 推荐学习路径

入门(本教程)
  ↓
Express 深入 + MongoDB/MySQL
  ↓
认证系统(JWT)
  ↓
测试驱动开发
  ↓
TypeScript + Node.js
  ↓
微服务架构
  ↓
容器化部署(Docker + K8s)

13.3 有用的资源

  • Node.js 官方文档:https://nodejs.org/docs
  • Express 官方文档:https://expressjs.com
  • MDN Web Docs:https://developer.mozilla.org/zh-CN/
  • npm 官网:https://www.npmjs.com

总结

本教程从零开始,带你走过了 Node.js 后端开发的完整学习路径:

  1. 基础概念:了解了 Node.js 是什么、为什么用它
  2. 环境搭建:安装 Node.js,了解 REPL 和项目初始化
  3. 模块系统:掌握了 CommonJS 和 ES Module 两种模块化方式
  4. 文件操作:学会了文件和目录的读写操作
  5. HTTP 服务:用原生模块创建了 Web 服务器
  6. Express 框架:掌握了路由、中间件、错误处理
  7. REST API:学会了设计和实现规范的 RESTful API
  8. 数据库:了解了 MongoDB、MySQL、SQLite 三种数据库的接入方式
  9. 实战项目:完成了一个功能完整的待办事项 API

最重要的是动手实践。看完教程后,试着自己从零搭建一个 API 服务,加入自己的想法和功能。遇到问题时,善用搜索引擎和官方文档,这是每个开发者的日常。

祝你学习愉快,编码顺利!🚀

内容声明

本文内容为AI技术学习教程,仅供学习参考。如涉及技术问题,欢迎通过 xurj005@163.com 与我们交流。

目录