使用 Node.js 中的 Apollo Server 创建 GraphQL API
引言
大家好,欢迎来到今天的讲座!今天我们要聊的是如何使用 Node.js 和 Apollo Server 来创建一个强大的 GraphQL API。如果你对 GraphQL 还不太熟悉,别担心,我会从头开始解释,确保每个人都能跟上节奏。我们还会通过一些实际的例子和代码片段来加深理解,让你在不知不觉中成为一名 GraphQL 大师 😎。
什么是 GraphQL?
GraphQL 是一种用于 API 的查询语言,它允许客户端精确地请求所需的数据,而不需要像 REST 那样每次都返回固定的数据结构。想象一下,你去餐厅点餐,传统的 REST API 就像是服务员给你端来一份固定的套餐,不管你是否需要所有的菜品;而 GraphQL 则像是你可以自由选择菜品,只点你想要的东西。这样不仅减少了不必要的数据传输,还能提高性能和灵活性。
为什么选择 Apollo Server?
Apollo Server 是一个非常流行的 GraphQL 服务器实现,它提供了丰富的功能和良好的社区支持。它不仅可以与 Node.js 完美集成,还支持多种数据库和身份验证方式。更重要的是,Apollo Server 的文档非常详细,社区也非常活跃,遇到问题时很容易找到解决方案。
本文的目标
通过今天的讲座,我们将一步步搭建一个完整的 GraphQL API。我们会涵盖以下内容:
- 安装和配置 Apollo Server
- 定义 Schema 和 Resolvers
- 连接数据库
- 添加身份验证
- 优化和调试
准备好了吗?让我们开始吧!🚀
1. 安装和配置 Apollo Server
1.1 创建一个新的 Node.js 项目
首先,我们需要创建一个新的 Node.js 项目。打开你的终端,进入你想要存放项目的文件夹,然后运行以下命令来初始化一个新的项目:
mkdir my-graphql-api
cd my-graphql-api
npm init -y
这会创建一个 package.json
文件,里面包含了项目的元数据。接下来,我们需要安装 Apollo Server 和其他必要的依赖项。
1.2 安装 Apollo Server 和 Express
为了简化开发,我们将使用 Express 作为 HTTP 服务器,并通过 Apollo Server 提供 GraphQL 端点。运行以下命令来安装所需的包:
npm install apollo-server-express express graphql
apollo-server-express
:这是 Apollo Server 的 Express 集成包。express
:一个轻量级的 Node.js 框架,用于处理 HTTP 请求。graphql
:GraphQL 的核心库,包含解析器和其他工具。
1.3 设置基本的 Express 服务器
现在我们已经安装了所有需要的依赖项,接下来让我们编写一些代码来启动一个简单的 Express 服务器。在项目根目录下创建一个 index.js
文件,并添加以下代码:
const express = require('express');
const { ApolloServer } = require('apollo-server-express');
const app = express();
// 创建一个 Apollo Server 实例
const server = new ApolloServer({
typeDefs: `
type Query {
hello: String
}
`,
resolvers: {
Query: {
hello: () => 'Hello, world!',
},
},
});
// 将 Apollo Server 应用到 Express 中
server.applyMiddleware({ app });
// 启动服务器
app.listen({ port: 4000 }, () =>
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`)
);
这段代码做了几件事:
- 引入依赖:我们引入了
express
和apollo-server-express
。 - 创建 Express 应用:使用
express()
创建了一个新的 Express 应用实例。 - 创建 Apollo Server:我们创建了一个新的 Apollo Server 实例,并传入了两个重要的属性:
typeDefs
和resolvers
。typeDefs
定义了我们的 GraphQL 模式(Schema),而resolvers
则是处理查询的函数。 - 将 Apollo Server 应用到 Express:通过
applyMiddleware
方法,我们将 Apollo Server 的中间件应用到了 Express 中。 - 启动服务器:最后,我们启动了服务器,并监听 4000 端口。
1.4 测试 API
现在,你可以通过以下命令启动服务器:
node index.js
服务器启动后,打开浏览器并访问 http://localhost:4000/graphql
,你会看到 Apollo Server 的 Playground 界面。Playground 是一个内置的图形化工具,可以帮助你测试和调试 GraphQL 查询。
在 Playground 中,输入以下查询并点击“Execute”按钮:
query {
hello
}
你应该会看到如下响应:
{
"data": {
"hello": "Hello, world!"
}
}
恭喜你!你刚刚创建了一个简单的 GraphQL API,并成功执行了第一个查询 🎉。
2. 定义 Schema 和 Resolvers
2.1 什么是 Schema?
在 GraphQL 中,Schema 是 API 的核心部分,它定义了客户端可以请求的数据类型和操作。Schema 由三部分组成:
- Types:定义了数据的结构。例如,用户、文章、评论等。
- Queries:定义了客户端可以请求的操作。例如,获取用户列表、获取单个用户等。
- Mutations:定义了客户端可以执行的修改操作。例如,创建用户、更新用户等。
2.2 定义一个更复杂的 Schema
为了展示更多功能,我们将扩展之前的 Schema,添加一些更复杂的数据类型和操作。假设我们正在构建一个博客平台,用户可以发布文章,文章可以有多个标签。我们可以定义如下的 Schema:
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
tags: [Tag!]!
}
type Tag {
id: ID!
name: String!
}
type Query {
users: [User!]!
user(id: ID!): User
posts: [Post!]!
post(id: ID!): Post
tags: [Tag!]!
tag(id: ID!): Tag
}
type Mutation {
createUser(name: String!, email: String!): User
createPost(title: String!, content: String!, authorId: ID!, tagIds: [ID!]!): Post
createTag(name: String!): Tag
}
2.3 解释 Schema
- User 类型:表示一个用户,包含
id
、name
、email
和posts
字段。posts
是一个数组,表示该用户发布的所有文章。 - Post 类型:表示一篇文章,包含
id
、title
、content
、author
和tags
字段。author
是一个User
类型的对象,表示文章的作者;tags
是一个Tag
类型的数组,表示文章的标签。 - Tag 类型:表示一个标签,包含
id
和name
字段。 - Query 类型:定义了客户端可以执行的查询操作。例如,
users
返回所有用户,user
根据id
返回单个用户,posts
返回所有文章,等等。 - Mutation 类型:定义了客户端可以执行的修改操作。例如,
createUser
创建一个新用户,createPost
创建一篇新文章,createTag
创建一个新标签。
2.4 编写 Resolvers
Resolvers 是处理查询和修改操作的函数。每个字段都需要一个对应的 resolver 函数来返回数据。我们可以通过模拟数据来实现这些 resolver,稍后我们会将其替换为真实的数据库查询。
在 index.js
中,更新 resolvers
部分如下:
const users = [
{ id: '1', name: 'Alice', email: 'alice@example.com' },
{ id: '2', name: 'Bob', email: 'bob@example.com' },
];
const posts = [
{ id: '1', title: 'My First Post', content: 'This is my first post.', authorId: '1', tagIds: ['1'] },
{ id: '2', title: 'My Second Post', content: 'This is my second post.', authorId: '1', tagIds: ['1', '2'] },
{ id: '3', title: 'My Third Post', content: 'This is my third post.', authorId: '2', tagIds: ['2'] },
];
const tags = [
{ id: '1', name: 'JavaScript' },
{ id: '2', name: 'Node.js' },
];
const resolvers = {
Query: {
users: () => users,
user: (_, { id }) => users.find(user => user.id === id),
posts: () => posts,
post: (_, { id }) => posts.find(post => post.id === id),
tags: () => tags,
tag: (_, { id }) => tags.find(tag => tag.id === id),
},
Mutation: {
createUser: (_, { name, email }) => {
const newUser = { id: (users.length + 1).toString(), name, email };
users.push(newUser);
return newUser;
},
createPost: (_, { title, content, authorId, tagIds }) => {
const newPost = { id: (posts.length + 1).toString(), title, content, authorId, tagIds };
posts.push(newPost);
return newPost;
},
createTag: (_, { name }) => {
const newTag = { id: (tags.length + 1).toString(), name };
tags.push(newTag);
return newTag;
},
},
Post: {
author: (parent) => users.find(user => user.id === parent.authorId),
tags: (parent) => parent.tagIds.map(tagId => tags.find(tag => tag.id === tagId)),
},
};
2.5 解释 Resolvers
- Query Resolvers:每个查询都有一个对应的 resolver 函数。例如,
users
返回所有用户,user
根据id
返回单个用户,posts
返回所有文章,等等。 - Mutation Resolvers:每个修改操作也有一个对应的 resolver 函数。例如,
createUser
创建一个新用户,createPost
创建一篇新文章,createTag
创建一个新标签。 - Field Resolvers:对于嵌套字段(如
Post
类型中的author
和tags
),我们也需要编写 resolver 函数。这些函数会在查询时自动调用,以返回相关联的数据。
2.6 测试新的 API
现在,你可以再次启动服务器并访问 http://localhost:4000/graphql
。你可以尝试执行以下查询和修改操作:
查询所有用户
query {
users {
id
name
email
}
}
查询单个用户
query {
user(id: "1") {
id
name
email
}
}
查询所有文章
query {
posts {
id
title
author {
id
name
}
tags {
id
name
}
}
}
创建新用户
mutation {
createUser(name: "Charlie", email: "charlie@example.com") {
id
name
email
}
}
创建新文章
mutation {
createPost(title: "My Fourth Post", content: "This is my fourth post.", authorId: "1", tagIds: ["1", "2"]) {
id
title
author {
id
name
}
tags {
id
name
}
}
}
3. 连接数据库
3.1 为什么要连接数据库?
到目前为止,我们一直在使用模拟数据来测试 API。虽然这对于开发和调试非常有用,但在生产环境中,我们通常需要将数据存储在数据库中。Apollo Server 支持多种数据库,包括 MongoDB、MySQL、PostgreSQL 等。今天我们将会使用 MongoDB 作为示例。
3.2 安装 MongoDB 和 Mongoose
首先,我们需要安装 MongoDB 和 Mongoose。Mongoose 是一个用于 MongoDB 的对象数据建模(ODM)库,它可以帮助我们更方便地与数据库交互。
运行以下命令来安装 MongoDB 和 Mongoose:
npm install mongoose
3.3 连接到 MongoDB
在 index.js
中,添加以下代码来连接到 MongoDB:
const mongoose = require('mongoose');
// 连接到 MongoDB 数据库
mongoose.connect('mongodb://localhost:27017/my-graphql-api', {
useNewUrlParser: true,
useUnifiedTopology: true,
});
// 监听连接状态
mongoose.connection.on('connected', () => {
console.log('Connected to MongoDB');
});
mongoose.connection.on('error', (err) => {
console.error('MongoDB connection error:', err);
});
3.4 定义 Mongoose 模型
接下来,我们需要定义 Mongoose 模型,以便将 GraphQL 类型映射到 MongoDB 集合。在项目根目录下创建一个 models
文件夹,并在其中创建三个文件:User.js
、Post.js
和 Tag.js
。
User 模型
// models/User.js
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
posts: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Post' }],
});
module.exports = mongoose.model('User', userSchema);
Post 模型
// models/Post.js
const mongoose = require('mongoose');
const postSchema = new mongoose.Schema({
title: { type: String, required: true },
content: { type: String, required: true },
author: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
tags: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Tag' }],
});
module.exports = mongoose.model('Post', postSchema);
Tag 模型
// models/Tag.js
const mongoose = require('mongoose');
const tagSchema = new mongoose.Schema({
name: { type: String, required: true, unique: true },
});
module.exports = mongoose.model('Tag', tagSchema);
3.5 更新 Resolvers
现在,我们需要更新 resolvers,使其从 MongoDB 中获取数据。首先,在 index.js
中引入 Mongoose 模型:
const User = require('./models/User');
const Post = require('./models/Post');
const Tag = require('./models/Tag');
然后,更新 resolvers
部分,使用 Mongoose 查询来代替模拟数据:
const resolvers = {
Query: {
users: async () => await User.find().populate('posts'),
user: async (_, { id }) => await User.findById(id).populate('posts'),
posts: async () => await Post.find().populate('author').populate('tags'),
post: async (_, { id }) => await Post.findById(id).populate('author').populate('tags'),
tags: async () => await Tag.find(),
tag: async (_, { id }) => await Tag.findById(id),
},
Mutation: {
createUser: async (_, { name, email }) => {
const newUser = new User({ name, email });
await newUser.save();
return newUser;
},
createPost: async (_, { title, content, authorId, tagIds }) => {
const author = await User.findById(authorId);
if (!author) throw new Error('Author not found');
const tags = await Tag.find({ _id: { $in: tagIds } });
if (tagIds.length !== tags.length) throw new Error('Some tags not found');
const newPost = new Post({ title, content, author, tags });
await newPost.save();
author.posts.push(newPost._id);
await author.save();
return newPost;
},
createTag: async (_, { name }) => {
const newTag = new Tag({ name });
await newTag.save();
return newTag;
},
},
Post: {
author: (parent) => parent.author,
tags: (parent) => parent.tags,
},
};
3.6 测试数据库连接
现在,你可以再次启动服务器并尝试执行一些查询和修改操作。你应该能够看到数据被保存到 MongoDB 中,并且可以从数据库中检索出来。
4. 添加身份验证
4.1 为什么要添加身份验证?
在大多数情况下,我们不希望任何人都可以随意创建或修改数据。因此,我们需要为 API 添加身份验证机制,以确保只有授权用户才能执行某些操作。今天我们将会使用 JWT(JSON Web Token)来进行身份验证。
4.2 安装 JWT 依赖
首先,我们需要安装 jsonwebtoken
和 bcryptjs
依赖。jsonwebtoken
用于生成和验证 JWT,而 bcryptjs
用于加密和验证密码。
运行以下命令来安装这些依赖:
npm install jsonwebtoken bcryptjs
4.3 创建用户注册和登录功能
在 Mutation
中,添加两个新的操作:register
和 login
。register
用于创建新用户并生成 JWT,login
用于验证用户凭据并返回 JWT。
更新 resolvers
部分如下:
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const SECRET_KEY = 'your-secret-key'; // 请使用更安全的密钥
const resolvers = {
// ... 其他 resolvers ...
Mutation: {
// ... 其他 mutations ...
register: async (_, { name, email, password }) => {
const existingUser = await User.findOne({ email });
if (existingUser) throw new Error('User already exists');
const hashedPassword = await bcrypt.hash(password, 10);
const newUser = new User({ name, email, password: hashedPassword });
await newUser.save();
const token = jwt.sign({ userId: newUser._id }, SECRET_KEY, { expiresIn: '1h' });
return { token, user: newUser };
},
login: async (_, { email, password }) => {
const user = await User.findOne({ email });
if (!user) throw new Error('User not found');
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) throw new Error('Invalid password');
const token = jwt.sign({ userId: user._id }, SECRET_KEY, { expiresIn: '1h' });
return { token, user };
},
},
};
4.4 保护路由
为了确保只有授权用户才能执行某些操作,我们需要在 resolvers 中添加身份验证逻辑。我们可以通过检查请求头中的 JWT 来验证用户身份。
在 index.js
中,添加一个中间件来解析 JWT:
const jwt = require('jsonwebtoken');
// 解析 JWT 的中间件
const authMiddleware = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (token) {
try {
const decoded = jwt.verify(token, SECRET_KEY);
req.userId = decoded.userId;
} catch (err) {
console.error('Invalid token:', err);
}
}
next();
};
// 将中间件应用到 Express 中
app.use(authMiddleware);
// 在 Apollo Server 中添加上下文
server.applyMiddleware({
app,
context: ({ req }) => ({
userId: req.userId,
}),
});
4.5 保护特定的 Resolver
现在,我们可以在特定的 resolver 中检查 userId
,以确保只有授权用户才能执行某些操作。例如,我们可以在 createPost
中添加身份验证逻辑:
Mutation: {
// ... 其他 mutations ...
createPost: async (_, { title, content, tagIds }, { userId }) => {
if (!userId) throw new Error('Not authenticated');
const author = await User.findById(userId);
if (!author) throw new Error('Author not found');
const tags = await Tag.find({ _id: { $in: tagIds } });
if (tagIds.length !== tags.length) throw new Error('Some tags not found');
const newPost = new Post({ title, content, author, tags });
await newPost.save();
author.posts.push(newPost._id);
await author.save();
return newPost;
},
},
4.6 测试身份验证
现在,你可以尝试注册一个新用户并登录。登录后,你会收到一个 JWT。你可以将这个 JWT 添加到后续请求的 Authorization
头中,以验证身份。
例如,在 Playground 中,你可以使用以下命令进行登录:
mutation {
login(email: "alice@example.com", password: "password") {
token
user {
id
name
email
}
}
}
然后,复制返回的 token
,并在后续请求中添加 Authorization: Bearer <token>
头。例如:
mutation {
createPost(title: "My Fifth Post", content: "This is my fifth post.", tagIds: ["1", "2"]) {
id
title
author {
id
name
}
tags {
id
name
}
}
}
5. 优化和调试
5.1 使用 GraphQL Playground
GraphQL Playground 是一个非常有用的工具,它可以帮助你测试和调试 API。除了基本的查询和修改操作外,Playground 还提供了许多其他功能,例如:
- Schema 文档:你可以查看 API 的完整 Schema 文档,了解所有可用的类型和字段。
- 变量支持:你可以使用变量来动态传递参数,而不必每次都手动修改查询。
- 错误处理:当查询失败时,Playground 会显示详细的错误信息,帮助你快速定位问题。
5.2 使用 Apollo Studio
Apollo Studio 是一个更高级的工具,它可以帮助你监控和优化 API 性能。它提供了实时的查询统计、延迟分析和错误报告等功能。要使用 Apollo Studio,你需要在 Apollo Server 中启用 tracing 和 caching 功能。
在 index.js
中,更新 ApolloServer
实例的配置:
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: true,
playground: true,
tracing: true,
cacheControl: true,
});
5.3 使用环境变量
为了确保安全性,我们应该避免将敏感信息(如数据库连接字符串和 JWT 密钥)硬编码在代码中。相反,我们可以使用环境变量来管理这些配置。
首先,安装 dotenv
包:
npm install dotenv
然后,在项目根目录下创建一个 .env
文件,并添加以下内容:
MONGODB_URI=mongodb://localhost:27017/my-graphql-api
JWT_SECRET=your-secret-key
最后,在 index.js
中引入 dotenv
并加载环境变量:
require('dotenv').config();
mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
const SECRET_KEY = process.env.JWT_SECRET;
5.4 使用 TypeScript
如果你喜欢使用 TypeScript,Apollo Server 也支持 TypeScript。通过使用 TypeScript,你可以获得更好的类型检查和代码补全功能,从而减少错误并提高开发效率。
要将项目转换为 TypeScript,你可以按照以下步骤操作:
-
安装 TypeScript 和相关依赖:
npm install typescript @types/node @types/express @types/graphql apollo-server-express
-
创建
tsconfig.json
文件:{ "compilerOptions": { "target": "ES6", "module": "commonjs", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "outDir": "./dist" }, "include": ["src/**/*.ts"], "exclude": ["node_modules"] }
-
将
index.js
重命名为index.ts
,并根据需要调整代码。 -
使用
tsc
命令编译 TypeScript 代码:npx tsc
结语
恭喜你完成了今天的讲座!通过学习如何使用 Node.js 和 Apollo Server 创建 GraphQL API,你已经掌握了许多关键技能。我们从基础的安装和配置开始,逐步深入到更复杂的主题,如数据库集成、身份验证和优化。希望你能将这些知识应用到自己的项目中,创造出更加高效和灵活的 API。
如果你有任何问题或建议,欢迎随时提问!祝你在 GraphQL 的世界里玩得开心 🚀!
附录:常用 GraphQL 查询和修改操作
查询所有用户
query {
users {
id
name
email
posts {
id
title
}
}
}
查询单个用户
query {
user(id: "1") {
id
name
email
posts {
id
title
}
}
}
查询所有文章
query {
posts {
id
title
author {
id
name
}
tags {
id
name
}
}
}
创建新用户
mutation {
createUser(name: "Charlie", email: "charlie@example.com") {
id
name
email
}
}
创建新文章
mutation {
createPost(title: "My Sixth Post", content: "This is my sixth post.", authorId: "1", tagIds: ["1", "2"]) {
id
title
author {
id
name
}
tags {
id
name
}
}
}
注册新用户
mutation {
register(name: "Dave", email: "dave@example.com", password: "password") {
token
user {
id
name
email
}
}
}
登录用户
mutation {
login(email: "alice@example.com", password: "password") {
token
user {
id
name
email
}
}
}
感谢大家的参与!希望今天的讲座对你有所帮助。再见啦!👋