使用 Node.js 中的 Apollo Server 创建 GraphQL API

使用 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。我们会涵盖以下内容:

  1. 安装和配置 Apollo Server
  2. 定义 Schema 和 Resolvers
  3. 连接数据库
  4. 添加身份验证
  5. 优化和调试

准备好了吗?让我们开始吧!🚀


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}`)
);

这段代码做了几件事:

  1. 引入依赖:我们引入了 expressapollo-server-express
  2. 创建 Express 应用:使用 express() 创建了一个新的 Express 应用实例。
  3. 创建 Apollo Server:我们创建了一个新的 Apollo Server 实例,并传入了两个重要的属性:typeDefsresolverstypeDefs 定义了我们的 GraphQL 模式(Schema),而 resolvers 则是处理查询的函数。
  4. 将 Apollo Server 应用到 Express:通过 applyMiddleware 方法,我们将 Apollo Server 的中间件应用到了 Express 中。
  5. 启动服务器:最后,我们启动了服务器,并监听 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 类型:表示一个用户,包含 idnameemailposts 字段。posts 是一个数组,表示该用户发布的所有文章。
  • Post 类型:表示一篇文章,包含 idtitlecontentauthortags 字段。author 是一个 User 类型的对象,表示文章的作者;tags 是一个 Tag 类型的数组,表示文章的标签。
  • Tag 类型:表示一个标签,包含 idname 字段。
  • 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 类型中的 authortags),我们也需要编写 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.jsPost.jsTag.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 依赖

首先,我们需要安装 jsonwebtokenbcryptjs 依赖。jsonwebtoken 用于生成和验证 JWT,而 bcryptjs 用于加密和验证密码。

运行以下命令来安装这些依赖:

npm install jsonwebtoken bcryptjs

4.3 创建用户注册和登录功能

Mutation 中,添加两个新的操作:registerloginregister 用于创建新用户并生成 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,你可以按照以下步骤操作:

  1. 安装 TypeScript 和相关依赖:

    npm install typescript @types/node @types/express @types/graphql apollo-server-express
  2. 创建 tsconfig.json 文件:

    {
     "compilerOptions": {
       "target": "ES6",
       "module": "commonjs",
       "strict": true,
       "esModuleInterop": true,
       "skipLibCheck": true,
       "forceConsistentCasingInFileNames": true,
       "outDir": "./dist"
     },
     "include": ["src/**/*.ts"],
     "exclude": ["node_modules"]
    }
  3. index.js 重命名为 index.ts,并根据需要调整代码。

  4. 使用 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
    }
  }
}

感谢大家的参与!希望今天的讲座对你有所帮助。再见啦!👋

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注