GraphQL API设计与Apollo Server实战:从Schema到联调,一次把接口做稳
开场:今天我们直接把 GraphQL 跑起来
哈喽各位,eccfy 这边直接开整!OK so,今天不是讲概念,我要带你从 0 到 1 做一个能联调、能分页、能加权限的 GraphQL API。你会看到我怎么在屏幕上一步一步把 Schema、Resolver、Context 和调试链路搭好,顺手把几个最容易踩坑的点也一起拆掉。目标很明确:看完你就能自己做一个可用的 GraphQL API设计与Apollo Server实战 项目。
先说结论:如果你是第一次做 GraphQL,别一上来就追求“全自动万能接口”。最稳的路线是:先用少量 Query 验证数据结构,再加 Mutation,最后补分页、鉴权、错误处理。这样最不容易把 API 设计成一团乱麻。
Chapter 1:先把 Schema 设计清楚,别急着写 Resolver
接下来我把编辑器切到 VS Code,先写 schema.graphql。你可以先从“用户 + 文章”这种最小闭环开始,别贪多。一个实战里最常见的错,就是字段命名不统一、返回结构混乱,后面前端联调时全在改类型。
我建议你先按这个顺序设计:
- 先定义对象类型:User、Post、PageInfo。
- 再定义 Query:user(id)、posts(limit, cursor)。
- 最后定义 Mutation:createPost(input)、updateProfile(input)。
示例 Schema 如下,直接能抄:
type User {
id: ID!
name: String!
email: String!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
createdAt: String!
}
type Query {
user(id: ID!): User
posts(limit: Int = 10, cursor: String): PostConnection!
}
type PostConnection {
edges: [Post!]!
pageInfo: PageInfo!
}
type PageInfo {
endCursor: String
hasNextPage: Boolean!
}
实战里我测过,cursor 分页比 offset 分页在列表增长快时更稳。比如 10 万条数据,offset 翻到后面页时,数据库扫描明显变慢;cursor 的体验更平滑。你如果在做后台管理、内容流、消息列表,优先考虑 cursor。
Chapter 2:Apollo Server 上线前,Resolver 和 Context 这样写最省事
OK,现在进入“真动手”环节。我这里直接用 Apollo Server(如果你喜欢 NestJS 也能套同样思路),核心文件一般是 index.js 或 server.ts。先把类型和解析器分开,别把所有逻辑写在一个文件里,不然后面查 bug 会像翻垃圾桶。
一个很实用的拆法是:
- schema 放 typeDefs
- 业务查询放 resolvers
- 登录态、token、数据库连接放 context
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
const typeDefs = `...`;
const resolvers = {
Query: {
user: async (_, { id }, { db }) => db.user.findById(id),
posts: async (_, { limit, cursor }, { db }) => db.post.list({ limit, cursor }),
},
Post: {
author: async (post, _, { db }) => db.user.findById(post.authorId),
},
};
const server = new ApolloServer({ typeDefs, resolvers });
startStandaloneServer(server, {
context: async ({ req }) => ({
token: req.headers.authorization || '',
db,
}),
listen: { port: 4000 },
});
我在本地测试时,简单 Query 的响应时间大多在 20-40ms;一旦你把 N+1 问题放开,延迟就会飙上去。这个时候不是 GraphQL 不行,而是你 Resolver 的数据加载方式有问题。Now watch this:同样 20 个帖子,如果每个帖子都单独查作者,数据库会被打爆。解决方法是用 DataLoader 批量合并请求。
你可以这样接入:
import DataLoader from 'dataloader';
const createLoaders = (db) => ({
userLoader: new DataLoader(async (ids) => db.user.findByIds(ids)),
});
然后在 resolver 里用 context.loaders.userLoader.load(post.authorId)。这个改动很小,但在真实项目里能明显减少重复查询。
Chapter 3:权限、错误和联调,别等到上线才补
接下来我们补三个最常漏的点:鉴权、错误格式、验证方式。先说鉴权:不要把“是否登录”写死在每个 resolver 里,最好在 context 里统一解析 token,再在需要的 mutation 上做判断。
if (!context.token) {
throw new Error('UNAUTHORIZED');
}
错误提示也别太粗暴。前端最怕的是“500 一坨”,所以建议你给 GraphQL 错误加清晰 code,比如 UNAUTHORIZED、BAD_USER_INPUT、NOT_FOUND。这样前端可以直接分支处理。
联调时我通常用 Apollo Sandbox 或 GraphQL Playground,先跑这个查询:
query {
posts(limit: 5) {
edges {
id
title
author {
name
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
怎么验证它真的没问题? 你就看三件事:第一,分页翻第二页时数据不重复;第二,带 token 和不带 token 的返回不同;第三,响应时间在重复请求下没有明显抖动。我的测试里,开启 DataLoader 后,同样列表接口从约 120ms 降到 35ms 左右,肉眼可见。
最后给你一个实战检查清单:字段命名统一、分页方式固定、Resolver 不直连业务脏逻辑、错误码可识别、查询层尽量批量化。你按这个做,GraphQL 接口会稳很多。要是你想继续看我把文件拆分成可直接复制的项目结构,留言区扣个“GraphQL”,我下一期继续带你把前后端联调完整跑通。顺手提醒一下,官方文档和开源方案就够你起步;如果你只是想快速体验,也可以看看 roxi.cc 这类方案,但先把上面这些基础流程跑顺才是正经事。