Node.js 构建 RESTful API 的基础方法
理解 RESTful API
REST 架构风格简介
REST(Representational State Transfer)是一种软件架构风格,由 Roy Fielding 在 2000 年的博士论文中提出。它旨在通过网络应用程序之间的交互,以一种简洁、可扩展的方式进行通信。REST 架构风格强调以下几个关键原则:
- 资源:REST 中的一切皆为资源。资源可以是任何具有标识的事物,例如一篇文章、一个用户、一个订单等。每个资源都有唯一的标识符(通常是一个 URL)。
- 统一接口:REST 定义了一组统一的接口,用于对资源进行操作。这些接口基于 HTTP 协议的方法,如 GET、POST、PUT、DELETE 等。这种统一接口使得不同的客户端和服务器能够以一致的方式进行交互。
- 无状态:在 REST 架构中,服务器不会在请求之间保存客户端的状态。每个请求都应该包含服务器处理该请求所需的所有信息。这种无状态性使得系统更容易扩展和维护。
- 分层系统:REST 架构允许将系统划分为多个层次,每个层次负责特定的功能。例如,客户端可以与代理服务器交互,代理服务器再与实际的服务器通信。这种分层结构提高了系统的可扩展性和安全性。
RESTful API 概述
RESTful API 是遵循 REST 架构风格的应用程序编程接口。它通过 HTTP 协议公开资源,并使用统一接口让客户端能够对这些资源进行创建、读取、更新和删除(CRUD)操作。例如,一个简单的博客 API 可能有以下资源和操作:
- 资源:
/articles
(表示所有文章),/articles/{id}
(表示特定文章,{id}
是文章的唯一标识符)。 - 操作:
GET /articles
:获取所有文章列表。GET /articles/{id}
:获取特定文章的详细信息。POST /articles
:创建一篇新文章。PUT /articles/{id}
:更新特定文章。DELETE /articles/{id}
:删除特定文章。
Node.js 基础
Node.js 简介
Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境,它允许开发人员在服务器端使用 JavaScript 编写代码。Node.js 采用事件驱动、非阻塞 I/O 模型,这使得它非常适合构建高性能、可扩展的网络应用程序,特别是在处理大量并发连接时表现出色。例如,一个简单的 Node.js HTTP 服务器可以这样创建:
const http = require('http');
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello, World!\n');
});
const port = 3000;
server.listen(port, () => {
console.log(`Server running at port ${port}`);
});
在上述代码中,我们使用 Node.js 内置的 http
模块创建了一个简单的 HTTP 服务器。该服务器监听在 3000 端口,当收到请求时,返回 "Hello, World!"。
Node.js 模块系统
Node.js 采用了模块化的设计理念,这有助于将代码组织成可复用的单元。Node.js 中有两种主要的模块类型:核心模块和用户自定义模块。
- 核心模块:这些是 Node.js 内置的模块,例如
http
、fs
(文件系统)、path
等。可以直接使用require
方法引入核心模块,如const http = require('http');
。 - 用户自定义模块:开发人员可以创建自己的模块。例如,创建一个名为
mathUtils.js
的文件:
// mathUtils.js
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
module.exports = {
add,
subtract
};
然后在另一个文件中可以这样引入并使用:
const mathUtils = require('./mathUtils');
const result1 = mathUtils.add(5, 3);
const result2 = mathUtils.subtract(5, 3);
console.log(result1); // 输出 8
console.log(result2); // 输出 2
在上述代码中,我们定义了一个自定义模块 mathUtils
,并通过 module.exports
将 add
和 subtract
函数暴露出去,供其他模块使用。
构建 RESTful API 的基础方法
选择合适的框架
虽然可以使用 Node.js 内置的 http
模块来构建 RESTful API,但这样做需要编写大量的样板代码。因此,通常会选择一个流行的框架来简化开发过程。以下是一些常用的 Node.js 框架:
- Express:Express 是 Node.js 中最流行的 web 应用框架之一。它提供了简洁的路由系统、中间件支持以及方便的 HTTP 方法处理。安装 Express 可以使用
npm install express
。以下是一个使用 Express 创建简单 RESTful API 的示例:
const express = require('express');
const app = express();
const port = 3000;
// 模拟数据
const articles = [
{ id: 1, title: 'Article 1', content: 'Content of article 1' },
{ id: 2, title: 'Article 2', content: 'Content of article 2' }
];
// 获取所有文章
app.get('/articles', (req, res) => {
res.json(articles);
});
// 获取特定文章
app.get('/articles/:id', (req, res) => {
const article = articles.find(a => a.id === parseInt(req.params.id));
if (article) {
res.json(article);
} else {
res.status(404).send('Article not found');
}
});
// 创建新文章
app.post('/articles', (req, res) => {
const newArticle = {
id: articles.length + 1,
title: req.body.title,
content: req.body.content
};
articles.push(newArticle);
res.status(201).json(newArticle);
});
// 更新文章
app.put('/articles/:id', (req, res) => {
const index = articles.findIndex(a => a.id === parseInt(req.params.id));
if (index!== -1) {
articles[index].title = req.body.title;
articles[index].content = req.body.content;
res.json(articles[index]);
} else {
res.status(404).send('Article not found');
}
});
// 删除文章
app.delete('/articles/:id', (req, res) => {
const index = articles.findIndex(a => a.id === parseInt(req.params.id));
if (index!== -1) {
const deletedArticle = articles[index];
articles.splice(index, 1);
res.json(deletedArticle);
} else {
res.status(404).send('Article not found');
}
});
app.listen(port, () => {
console.log(`Server running at port ${port}`);
});
在上述代码中,我们使用 Express 框架创建了一个简单的文章管理 API。定义了获取所有文章、获取特定文章、创建新文章、更新文章和删除文章的路由。
2. Koa:Koa 是由 Express 原班人马打造的新一代 web 框架。它使用了 ES6 的 Generator 函数和 async/await 语法,使得异步代码的编写更加简洁和可读。安装 Koa 可以使用 npm install koa
。以下是一个简单的 Koa RESTful API 示例:
const Koa = require('koa');
const app = new Koa();
const port = 3000;
// 模拟数据
const articles = [
{ id: 1, title: 'Article 1', content: 'Content of article 1' },
{ id: 2, title: 'Article 2', content: 'Content of article 2' }
];
// 获取所有文章
app.use(async (ctx) => {
if (ctx.path === '/articles' && ctx.method === 'GET') {
ctx.body = articles;
}
});
// 获取特定文章
app.use(async (ctx) => {
if (ctx.path.match(/^\/articles\/\d+$/) && ctx.method === 'GET') {
const id = parseInt(ctx.path.split('/')[2]);
const article = articles.find(a => a.id === id);
if (article) {
ctx.body = article;
} else {
ctx.status = 404;
ctx.body = 'Article not found';
}
}
});
// 创建新文章
app.use(async (ctx) => {
if (ctx.path === '/articles' && ctx.method === 'POST') {
const newArticle = {
id: articles.length + 1,
title: ctx.request.body.title,
content: ctx.request.body.content
};
articles.push(newArticle);
ctx.status = 201;
ctx.body = newArticle;
}
});
// 更新文章
app.use(async (ctx) => {
if (ctx.path.match(/^\/articles\/\d+$/) && ctx.method === 'PUT') {
const id = parseInt(ctx.path.split('/')[2]);
const index = articles.findIndex(a => a.id === id);
if (index!== -1) {
articles[index].title = ctx.request.body.title;
articles[index].content = ctx.request.body.content;
ctx.body = articles[index];
} else {
ctx.status = 404;
ctx.body = 'Article not found';
}
}
});
// 删除文章
app.use(async (ctx) => {
if (ctx.path.match(/^\/articles\/\d+$/) && ctx.method === 'DELETE') {
const id = parseInt(ctx.path.split('/')[2]);
const index = articles.findIndex(a => a.id === id);
if (index!== -1) {
const deletedArticle = articles[index];
articles.splice(index, 1);
ctx.body = deletedArticle;
} else {
ctx.status = 404;
ctx.body = 'Article not found';
}
}
});
app.listen(port, () => {
console.log(`Server running at port ${port}`);
});
在上述代码中,我们使用 Koa 框架实现了类似的文章管理 API。Koa 通过 app.use
方法来注册中间件,处理不同的 HTTP 请求。
处理路由
路由是 RESTful API 的关键部分,它决定了如何将不同的 HTTP 请求映射到相应的处理函数。
- Express 路由:在 Express 中,可以使用
app.METHOD(path, handler)
语法来定义路由,其中METHOD
是 HTTP 方法(如get
、post
、put
、delete
等),path
是 URL 路径,handler
是处理请求的函数。例如:
app.get('/articles', (req, res) => {
// 处理获取所有文章的逻辑
res.json(articles);
});
Express 还支持路由参数,如 app.get('/articles/:id', (req, res) => {... })
,其中 :id
是一个参数,在处理函数中可以通过 req.params.id
获取其值。
2. Koa 路由:在 Koa 中,通常需要借助第三方库来实现路由功能,例如 koa - router
。首先安装 koa - router
:npm install koa - router
。然后使用如下代码:
const Koa = require('koa');
const Router = require('koa - router');
const app = new Koa();
const router = new Router();
// 模拟数据
const articles = [
{ id: 1, title: 'Article 1', content: 'Content of article 1' },
{ id: 2, title: 'Article 2', content: 'Content of article 2' }
];
// 获取所有文章
router.get('/articles', (ctx) => {
ctx.body = articles;
});
// 获取特定文章
router.get('/articles/:id', (ctx) => {
const id = parseInt(ctx.params.id);
const article = articles.find(a => a.id === id);
if (article) {
ctx.body = article;
} else {
ctx.status = 404;
ctx.body = 'Article not found';
}
});
app.use(router.routes());
app.use(router.allowedMethods());
const port = 3000;
app.listen(port, () => {
console.log(`Server running at port ${port}`);
});
在上述代码中,我们使用 koa - router
库为 Koa 应用定义了路由。通过 router.METHOD(path, handler)
方式定义路由,最后通过 app.use(router.routes())
和 app.use(router.allowedMethods())
将路由应用到 Koa 应用中。
解析请求数据
在 RESTful API 中,经常需要从请求中获取数据,例如创建新资源时从请求体中获取数据。
- Express 解析请求数据:Express 可以使用中间件来解析请求数据。对于 JSON 格式的数据,可以使用
express.json()
中间件;对于 URL - encoded 格式的数据,可以使用express.urlencoded({ extended: true })
中间件。例如:
const express = require('express');
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.post('/articles', (req, res) => {
const newArticle = {
id: 1,
title: req.body.title,
content: req.body.content
};
// 处理创建文章逻辑
res.json(newArticle);
});
在上述代码中,express.json()
和 express.urlencoded({ extended: true })
中间件被添加到应用中,这样在处理 POST
请求时,就可以通过 req.body
获取请求体中的数据。
2. Koa 解析请求数据:Koa 没有内置直接解析请求数据的中间件,但可以使用第三方库,如 koa - bodyparser
。首先安装 koa - bodyparser
:npm install koa - bodyparser
。然后使用如下代码:
const Koa = require('koa');
const bodyParser = require('koa - bodyparser');
const app = new Koa();
app.use(bodyParser());
app.post('/articles', async (ctx) => {
const newArticle = {
id: 1,
title: ctx.request.body.title,
content: ctx.request.body.content
};
// 处理创建文章逻辑
ctx.body = newArticle;
});
在上述代码中,通过 app.use(bodyParser())
将 koa - bodyparser
中间件添加到 Koa 应用中,使得可以通过 ctx.request.body
获取请求体中的数据。
响应处理
- 返回正确的 HTTP 状态码:在 RESTful API 中,返回正确的 HTTP 状态码非常重要,它可以让客户端了解请求的处理结果。例如:
- 200 OK:请求成功,通常用于
GET
请求成功获取资源。 - 201 Created:资源创建成功,通常用于
POST
请求创建新资源。 - 400 Bad Request:客户端请求有误,例如请求参数缺失或格式不正确。
- 404 Not Found:请求的资源不存在。
- 500 Internal Server Error:服务器内部错误。
在 Express 中,可以使用
res.status(code).send(message)
或res.status(code).json(data)
来设置状态码并返回响应。例如:
app.get('/articles/:id', (req, res) => {
const article = articles.find(a => a.id === parseInt(req.params.id));
if (article) {
res.status(200).json(article);
} else {
res.status(404).send('Article not found');
}
});
在 Koa 中,可以使用 ctx.status = code
和 ctx.body = data
来设置状态码并返回响应。例如:
router.get('/articles/:id', async (ctx) => {
const id = parseInt(ctx.params.id);
const article = articles.find(a => a.id === id);
if (article) {
ctx.status = 200;
ctx.body = article;
} else {
ctx.status = 404;
ctx.body = 'Article not found';
}
});
- 处理响应格式:常见的响应格式有 JSON、XML 等。在现代 RESTful API 开发中,JSON 是最常用的格式。在 Express 中,可以使用
res.json(data)
直接返回 JSON 格式的响应。在 Koa 中,设置ctx.body = data
时,如果数据是对象或数组,Koa 会自动将其转换为 JSON 格式并设置Content - Type
为application/json
。
数据存储与持久化
使用内存模拟数据存储
在开发初期或简单示例中,可以使用内存中的数据结构(如数组、对象)来模拟数据存储。例如,在前面的文章管理 API 示例中,我们使用一个数组来存储文章数据:
const articles = [
{ id: 1, title: 'Article 1', content: 'Content of article 1' },
{ id: 2, title: 'Article 2', content: 'Content of article 2' }
];
这种方式简单方便,但数据在服务器重启后会丢失,不适合生产环境。
基于文件系统的数据存储
对于一些简单的应用,可以将数据存储在文件中。Node.js 的 fs
(文件系统)模块提供了操作文件的方法。例如,我们可以将文章数据存储在一个 JSON 文件中。以下是一个简单的示例,展示如何读取和写入文章数据到文件:
const fs = require('fs');
const path = require('path');
// 读取文章数据
function readArticles() {
try {
const data = fs.readFileSync(path.join(__dirname, 'articles.json'), 'utf8');
return JSON.parse(data);
} catch (err) {
return [];
}
}
// 写入文章数据
function writeArticles(articles) {
fs.writeFileSync(path.join(__dirname, 'articles.json'), JSON.stringify(articles, null, 2));
}
// 示例使用
const articles = readArticles();
const newArticle = { id: articles.length + 1, title: 'New Article', content: 'This is a new article' };
articles.push(newArticle);
writeArticles(articles);
在上述代码中,readArticles
函数用于从 articles.json
文件中读取文章数据,writeArticles
函数用于将文章数据写入该文件。
使用数据库
- 关系型数据库(如 MySQL、PostgreSQL):
- MySQL:可以使用
mysql
或mysql2
模块来连接和操作 MySQL 数据库。首先安装mysql2
:npm install mysql2
。以下是一个简单的示例,展示如何从 MySQL 数据库中查询文章数据:
const mysql = require('mysql2');
const connection = mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'password',
database: 'test'
});
connection.connect();
const query = 'SELECT * FROM articles';
connection.query(query, (err, results, fields) => {
if (err) throw err;
console.log(results);
});
connection.end();
在上述代码中,我们创建了一个 MySQL 连接,执行了一个查询语句,从 articles
表中获取所有文章数据。
- PostgreSQL:可以使用
pg
模块来连接和操作 PostgreSQL 数据库。首先安装pg
:npm install pg
。以下是一个简单的示例:
const { Pool } = require('pg');
const pool = new Pool({
user: 'user',
host: 'localhost',
database: 'test',
password: 'password',
port: 5432
});
const query = 'SELECT * FROM articles';
pool.query(query, (err, res) => {
if (err) {
console.error(err);
return;
}
console.log(res.rows);
pool.end();
});
在上述代码中,我们使用 pg
模块的 Pool
创建了一个连接池,执行查询并获取文章数据。
2. 非关系型数据库(如 MongoDB):
- MongoDB:可以使用
mongodb
模块来连接和操作 MongoDB 数据库。首先安装mongodb
:npm install mongodb
。以下是一个简单的示例,展示如何从 MongoDB 中查询文章数据:
const { MongoClient } = require('mongodb');
const uri = "mongodb://localhost:27017";
const client = new MongoClient(uri);
async function findArticles() {
try {
await client.connect();
const database = client.db('test');
const articlesCollection = database.collection('articles');
const result = await articlesCollection.find({}).toArray();
console.log(result);
} finally {
await client.close();
}
}
findArticles();
在上述代码中,我们使用 MongoClient
连接到 MongoDB,选择数据库和集合,执行查询获取所有文章数据。
错误处理
捕获同步错误
在 Node.js 中,同步代码中的错误可以使用传统的 try...catch
块来捕获。例如:
try {
const result = JSON.parse('{invalid json');
} catch (err) {
console.error('Error parsing JSON:', err);
}
在上述代码中,如果 JSON.parse
遇到无效的 JSON 字符串,会抛出一个错误,这个错误会被 catch
块捕获并处理。
捕获异步错误
- 基于回调的异步函数:对于基于回调的异步函数,错误通常作为回调函数的第一个参数传递。例如,
fs.readFile
是一个基于回调的异步函数:
const fs = require('fs');
fs.readFile('nonexistentFile.txt', 'utf8', (err, data) => {
if (err) {
console.error('Error reading file:', err);
return;
}
console.log('File content:', data);
});
在上述代码中,如果文件不存在或读取文件时发生错误,err
参数会被设置为错误对象,我们可以在回调函数中处理这个错误。
2. Promise - based 异步函数:对于返回 Promise 的异步函数,可以使用 .catch
方法来捕获错误。例如:
const fs = require('fs').promises;
fs.readFile('nonexistentFile.txt', 'utf8')
.then(data => {
console.log('File content:', data);
})
.catch(err => {
console.error('Error reading file:', err);
});
在上述代码中,fs.readFile
的 Promise 版本如果发生错误,会被 .catch
块捕获。
3. async/await:使用 async/await
语法时,可以使用 try...catch
块来捕获异步操作中的错误。例如:
const fs = require('fs').promises;
async function readFileContent() {
try {
const data = await fs.readFile('nonexistentFile.txt', 'utf8');
console.log('File content:', data);
} catch (err) {
console.error('Error reading file:', err);
}
}
readFileContent();
在上述代码中,await
后的异步操作如果抛出错误,会被 try...catch
块捕获。
在 RESTful API 中处理错误
在 RESTful API 开发中,需要将错误以合适的方式返回给客户端。例如,在 Express 中:
app.get('/articles/:id', (req, res) => {
try {
const article = articles.find(a => a.id === parseInt(req.params.id));
if (article) {
res.json(article);
} else {
res.status(404).send('Article not found');
}
} catch (err) {
res.status(500).send('Internal Server Error');
}
});
在 Koa 中:
router.get('/articles/:id', async (ctx) => {
try {
const id = parseInt(ctx.params.id);
const article = articles.find(a => a.id === id);
if (article) {
ctx.body = article;
} else {
ctx.status = 404;
ctx.body = 'Article not found';
}
} catch (err) {
ctx.status = 500;
ctx.body = 'Internal Server Error';
}
});
在上述代码中,我们捕获可能发生的错误,并返回相应的 HTTP 状态码和错误信息给客户端。
安全性考虑
输入验证
在接收客户端请求数据时,必须进行输入验证,以防止恶意数据的注入。例如,在创建文章时,确保文章标题和内容不为空:
// Express 示例
app.post('/articles', (req, res) => {
const { title, content } = req.body;
if (!title ||!content) {
return res.status(400).send('Title and content are required');
}
const newArticle = {
id: articles.length + 1,
title,
content
};
articles.push(newArticle);
res.status(201).json(newArticle);
});
// Koa 示例
router.post('/articles', async (ctx) => {
const { title, content } = ctx.request.body;
if (!title ||!content) {
ctx.status = 400;
ctx.body = 'Title and content are required';
return;
}
const newArticle = {
id: articles.length + 1,
title,
content
};
articles.push(newArticle);
ctx.status = 201;
ctx.body = newArticle;
});
在上述代码中,我们验证了文章的标题和内容是否为空,如果为空则返回 400 错误。
防止 SQL 注入(如果使用关系型数据库)
当使用关系型数据库时,SQL 注入是一个常见的安全风险。例如,在 MySQL 中使用 mysql2
模块时,可以使用参数化查询来防止 SQL 注入:
const mysql = require('mysql2');
const connection = mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'password',
database: 'test'
});
const articleId = 1;
const query = 'SELECT * FROM articles WHERE id =?';
connection.query(query, [articleId], (err, results, fields) => {
if (err) throw err;
console.log(results);
});
connection.end();
在上述代码中,?
是参数占位符,[articleId]
是参数值。这样可以确保传入的值不会被错误地解析为 SQL 语句的一部分,从而防止 SQL 注入。
防止 XSS 攻击(跨站脚本攻击)
XSS 攻击通常发生在将用户输入的数据直接输出到网页上。在 RESTful API 中,虽然不直接涉及网页渲染,但如果 API 的数据最终会在网页上使用,也需要防止 XSS 攻击。可以使用一些库(如 DOMPurify
)来清理用户输入的数据。例如:
const DOMPurify = require('dompurify');
const dirtyInput = '<script>alert("XSS")</script>';
const cleanInput = DOMPurify.sanitize(dirtyInput);
console.log(cleanInput); // 输出 <script>alert("XSS")</script>
在上述代码中,DOMPurify.sanitize
方法将危险的 HTML 和 JavaScript 代码进行了清理,防止了 XSS 攻击。
身份验证与授权
- 身份验证:常见的身份验证方式有 Basic 认证、Bearer 令牌认证等。例如,使用 JSON Web Tokens(JWT)进行身份验证。首先安装
jsonwebtoken
:npm install jsonwebtoken
。以下是一个简单的示例,展示如何生成和验证 JWT:
const jwt = require('jsonwebtoken');
// 生成 JWT
const payload = { userId: 1, username: 'user1' };
const secretKey = 'your - secret - key';
const token = jwt.sign(payload, secretKey, { expiresIn: '1h' });
// 验证 JWT
function verifyToken(req, res, next) {
const token = req.headers['authorization'];
if (!token) {
return res.status(401).send('Access denied. No token provided.');
}
try {
const decoded = jwt.verify(token.replace('Bearer ', ''), secretKey);
req.user = decoded;
next();
} catch (err) {
res.status(400).send('Invalid token');
}
}
在上述代码中,jwt.sign
用于生成 JWT,jwt.verify
用于验证 JWT。可以将 verifyToken
函数作为中间件,在需要身份验证的路由前使用。
2. 授权:授权是确定已认证用户是否有权执行特定操作的过程。例如,只有文章的作者才能更新或删除文章。可以在路由处理函数中进行授权检查:
// 假设文章数据包含作者信息
const articles = [
{ id: 1, title: 'Article 1', content: 'Content of article 1', author: 'user1' }
];
app.put('/articles/:id', verifyToken, (req, res) => {
const article = articles.find(a => a.id === parseInt(req.params.id));
if (article) {
if (article.author!== req.user.username) {
return res.status(403).send('Access denied. You are not the author.');
}
// 执行更新操作
article.title = req.body.title;
article.content = req.body.content;
res.json(article);
} else {
res.status(404).send('Article not found');
}
});
在上述代码中,在更新文章的路由处理函数中,首先检查用户是否是文章的作者,如果不是则返回 403 禁止访问错误。