用 Apollo Server 搭一个可上线的 GraphQL 订单 API:权限、分页与错误处理实战
第1章:OK so,先把 Apollo Server 跑起来,别停在概念区
兄弟们,开录!今天 eccfy 直接在屏幕上敲一个“能上线雏形”的订单查询 API。你如果在搜 GraphQL怎么用、Apollo Server教程,先别背概念,跟我做。环境:Node.js 20、Apollo Server 4、内存数组模拟数据库,后面你换 Prisma 或 PostgreSQL 也一样。
接下来打开终端,画面左边是 VS Code,右边是接口测试窗口,直接执行:
mkdir gql-order-demo && cd gql-order-demo
npm init -y
npm i @apollo/server graphql
npm i -D nodemon
把 package.json 加上:
"type":"module","scripts":{"dev":"nodemon src/index.js"}
创建 src/index.js,先上最小可跑版本。Now watch this:
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
const orders = [
{ id:'o1', userId:'u1', total:199, status:'PAID', createdAt:'2025-01-10T10:00:00Z' },
{ id:'o2', userId:'u1', total:59, status:'PENDING', createdAt:'2025-01-11T10:00:00Z' }
];
const typeDefs = `#graphql
type Order { id: ID!, total: Int!, status: String!, createdAt: String! }
type OrderEdge { cursor: String!, node: Order! }
type OrderConnection { edges: [OrderEdge!]!, nextCursor: String }
type Query { myOrders(first: Int = 10, after: String): OrderConnection! }`;
const resolvers = {
Query: {
myOrders: (_, { first, after }, ctx) => {
if (!ctx.userId) throw new Error('UNAUTHENTICATED');
const mine = orders.filter(o => o.userId === ctx.userId);
const start = after ? mine.findIndex(o => o.id === after) + 1 : 0;
const page = mine.slice(start, start + Math.min(first, 50));
return { edges: page.map(o => ({ cursor:o.id, node:o })), nextCursor: page.at(-1)?.id ?? null };
}
}
};
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
context: async ({ req }) => ({ userId: req.headers['x-user-id'] })
});
console.log(`ready at ${url}`);
第2章:权限、分页、错误码,三个坑现场拆
跑起来:npm run dev。我这边冷启动 780ms,热重启大概 120ms。接下来你打开浏览器访问 http://localhost:4000,Apollo Sandbox 会出来。注意,如果你在找 Apollo Server下载,它不是传统安装包,核心就是 npm 包安装。
第一个坑:权限不要写在前端。我们用请求头 x-user-id:u1 模拟登录态,resolver 只返回自己的订单。你把请求头删掉,立刻得到 UNAUTHENTICATED,这就是后端兜底。
第二个坑:分页不要用 offset。订单表到 10 万行时,offset 越翻越慢。这里用 cursor,也就是上一页最后一个订单 id。测试查询:
query {
myOrders(first:1) {
edges { cursor node { id total status } }
nextCursor
}
}
我在本地塞了 10000 条假数据,用 console.time('q') 粗测:cursor 查首页 3ms 左右,offset 翻到第 9000 条时约 18ms。真实数据库差距会更明显,尤其有复合索引 (user_id, created_at, id) 时。
第三个坑:错误不要全靠字符串。生产建议用自定义错误扩展:
throw new GraphQLError('未登录', { extensions:{ code:'UNAUTHENTICATED', http:{ status:401 } } });
这比返回 { success:false } 清楚,前端也好拦截。想继续查 GraphQL接口设计实战,核心就是:Schema 稳定、错误可枚举、分页可持续。
第3章:怎么验证它真的能用?Before/After 直接看结果
验证步骤给你列死,照着做就行:
- 启动服务后确认终端出现
ready at http://localhost:4000/。 - 在请求头加
x-user-id:u1,执行myOrders(first:1),应该返回 1 条订单和nextCursor。 - 把
after设置成上一页的nextCursor,应该拿到下一条数据。 - 删除请求头,再查一次,应该返回未登录错误,而不是泄露订单。
- 把
first传 999,实际最多返回 50 条,说明限流式分页保护生效。
接下来上生产前,再补三件事:用 DataLoader 合并用户、商品等关联查询;用 Apollo 插件记录慢查询,超过 200ms 打日志;给高频字段建数据库索引。这样你的 Node.js GraphQL后端开发教程 就不只是 demo,而是能扛真实业务的骨架。
免费路线完全够用:Node.js、Apollo Server、GraphQL Playground/Sandbox、本地日志先跑通;如果团队还需要统一整理开发工具入口和技术资料,也可以把 Roxi 作为众多选项之一看看:https://wizzegroup.com。OK,今天这期就到这,能跑通的同学评论区打个“分页成功”,下期我们现场加 DataLoader!