与 Express 集成
🎯 引言
前面我们一直用 startStandaloneServer 启动独立服务,适合学习和快速验证。但真实项目往往已经有一个 Express 服务在跑:里面有登录鉴权、日志中间件,还有一堆现成的 REST 接口。这时更合理的做法是把 GraphQL 作为一个路由挂载进去,而不是另起炉灶。
学完这篇文章,你能把 Apollo Server 集成到 Express 项目中,让 GraphQL 和 REST 接口共用一个端口,并学会用 context 把请求信息(比如请求头)传递给每个 resolver。这是把 GraphQL 用进真实项目的关键一步。
🧱 为什么用 expressMiddleware
Apollo Server 提供两种运行方式:
- 独立模式(
startStandaloneServer):自带 HTTP 服务,开箱即用,但没法加自己的路由和中间件。 - 中间件模式(
expressMiddleware):把 GraphQL 变成 Express 的一个中间件,挂在你指定的路径上,项目里其他逻辑照常工作。
从 Apollo Server 5 开始,Express 集成被拆成了独立的包,Express 5 对应 @as-integrations/express5。本文基于 @apollo/server 5.x、express 5.x、@as-integrations/express5 1.x 编写和验证,先安装:
npm install express @as-integrations/express5
🛠 把 GraphQL 挂到 Express 上
改造分三步:创建 Express 应用、启动 Apollo Server、把中间件挂到 /graphql 路径。完整代码如下:
import express from 'express';
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@as-integrations/express5';
const users = [
{ id: '1', username: '小明', age: 20 },
{ id: '2', username: '小美', age: 22 },
];
const typeDefs = `#graphql
type User {
id: ID!
username: String!
age: Int
}
type Query {
users: [User!]!
user(id: ID!): User
}
`;
const resolvers = {
Query: {
users: () => users,
user: (parent, args) => users.find((item) => item.id === args.id),
},
};
const app = express();
// 1. 创建并启动 Apollo Server
const server = new ApolloServer({ typeDefs, resolvers });
await server.start();
// 2. 把 GraphQL 挂载到 /graphql 路径
app.use('/graphql', express.json(), expressMiddleware(server));
// 3. 原有的 REST 接口照常工作
app.get('/hello', (req, res) => {
res.json({ message: 'hello' });
});
app.listen(4000, () => {
console.log('服务已启动:http://localhost:4000/graphql');
});
几个关键细节:
express.json():Express 的 JSON 解析中间件,必须放在expressMiddleware前面,GraphQL 请求体是 JSON,不解析就读不到内容。app.use('/graphql', ...):GraphQL 的入口变成了/graphql,调试工具地址也跟着变成http://localhost:4000/graphql。- 同一个 Express 实例上,REST 路由和 GraphQL 互不干扰,各走各的路径。
运行 node server.js 后验证:访问 http://localhost:4000/hello 返回 JSON,打开 http://localhost:4000/graphql 进入调试工具写查询,两条路都通,集成就完成了。
✨ 用 context 传递请求信息
实际项目里,resolver 经常需要「这次请求是谁发的」这类信息,最典型的就是登录态:从请求头里取出 token,判断当前用户身份。
GraphQL 用 **context(上下文)**解决这个问题。它是在 expressMiddleware 里定义的一个函数,每次请求都会执行一次,返回的对象会传给所有 resolver:
app.use(
'/graphql',
express.json(),
expressMiddleware(server, {
context: async ({ req }) => {
return {
token: req.headers.authorization || '',
};
},
})
);
resolver 通过第三个参数拿到 context:
const resolvers = {
Query: {
// 第三个参数 context 就是上面函数返回的对象
currentUser: (parent, args, context) => {
if (!context.token) {
return null;
}
// 真实项目在这里校验 token、查出当前用户
return users[0];
},
},
};
配合 Schema 加一个入口:
type Query {
users: [User!]!
currentUser: User
}
这样,无论多少个 resolver 需要登录态,都通过 context 统一获取,不用每个接口重复解析请求头。以后接数据库时,把数据库连接也放进 context,resolver 就能统一访问了。
🧾 小节总结
- 真实项目用中间件模式把 GraphQL 挂进 Express,和 REST 接口共用端口。
- Apollo Server 5 的 Express 集成在独立包里,Express 5 使用
@as-integrations/express5。 app.use('/graphql', express.json(), expressMiddleware(server)),express.json()必须在前。- context 函数每次请求执行一次,返回值传给所有 resolver 的第三个参数。
- 登录态、数据库连接等「每次请求都需要」的东西,都适合放进 context。
❓ 知识问答
Q1:忘了加 express.json() 会怎样?
Apollo Server 读不到解析后的请求体,会报请求格式相关的错误。只要看到 GraphQL 请求全部失败,先检查 JSON 解析中间件的位置。
Q2:能把 GraphQL 挂在根路径 / 吗?
可以,app.use('/', ...) 即可,但不推荐。挂成 /graphql 这样的独立路径,和 REST 路由界限清晰,也方便后续加鉴权等中间件。
Q3:REST 接口要全部迁移到 GraphQL 吗?
不用。两者可以长期共存,新功能用 GraphQL,旧的 REST 保持稳定运行,逐步过渡即可。
Q4:context 函数里能做异步操作吗?
可以,它支持 async,比如在里面查数据库验证 token。注意它每个请求都会执行,耗时操作会影响所有接口的响应速度。
Q5:standalone 模式和中间件模式的代码差别大吗?
typeDefs 和 resolvers 完全一样,只是启动部分从 startStandaloneServer 换成 Express 挂载。业务逻辑零改动,可以放心迁移。
🧪 小练习
练习一:在 Express 服务上加一个 REST 接口 GET /api/users 返回用户数组,确认它和 /graphql 的查询能同时正常工作。
// 请在这里编写代码
练习二:扩展 context,把请求头里的 x-user-id 传进去,然后实现 currentUser 查询:按这个 id 返回对应用户,没传或找不到时返回 null。
// 请在这里编写代码
🎉 恭喜你已经能把 GraphQL 集成进 Express 项目啦!最后一篇我们换个视角,看看前端怎么发起 GraphQL 请求,把整条链路彻底打通。
