REST 和 GraphQL 不是二选一:一次过度设计把简单查询做成灾难,和我们的取舍

📅 2026/8/2 4:50:38
REST 和 GraphQL 不是二选一:一次过度设计把简单查询做成灾难,和我们的取舍
技术选型圈最烦人的话就是XX 已死改用 YY。REST 和 GraphQL 这几年就被这么对待过。但真到了项目里这两者从来不是替代关系而是分别适配不同场景的工具。这篇文章用我们一次把简单后台查询硬改成 GraphQL、结果开发效率反而暴跌的复盘聊聊两者到底差在哪、什么情况该选谁。先把我的结论摆出来选 API 风格的标准只有一条——你的客户端需要多灵活地决定自己要什么而不是哪个听起来更先进。REST 的真相不是网址 JSON那么简单很多人以为写个RestController返回 JSON 就是 REST 了但真正的 REST 约束无状态、统一接口、HATEOAS大部分项目根本没遵守也不该遵守。我们生产里用的是 pragmatic REST——资源化 URL 状态码 分页RestController RequestMapping(/api/v1/orders) public class OrderApi { GetMapping public PageResultOrderDTO list( // ① 版本化路径避免破坏老客户端 RequestParam(defaultValue 0) int page, RequestParam(defaultValue 20) int size) { PageOrder p orderService.page(page, size); return PageResult.of(p.getContent(), p.getTotal()); // ② 统一分页结构 } GetMapping(/{id}) public ResponseEntityOrderDTO get(PathVariable Long id) { return orderService.find(id) .map(ResponseEntity::ok) // ③ 用标准状态码表达结果 .orElse(ResponseEntity.notFound().build()); // ④ 404 而非 200 错误码 } }几个我们吃过的亏① 路径带v1曾经因为不版本化改了返回结构直接把老 App 端搞崩紧急回滚了一整晚③ 和 ④ 用 HTTP 状态码而非自定义code字段网关层的限流、超时、重试才能正确识别失败——如果你把所有错误都包成 200 {code:500}网关的熔断和健康检查就全瞎了。REST 最大的优点就是简单、缓存友好、工具链成熟——浏览器、CDN、各类 SDK 天生认它。但它有个老毛病过度获取over-fetching和获取不足under-fetching。前端要一个订单的用户昵称REST 要么返回整个用户对象多传要么前端再发一次请求去查用户多查。GraphQL 的真相解决的是前端决定要什么GraphQL 的核心价值是让客户端声明自己要哪些字段服务端按需组装。我们用graphql-java实现过一个商品查询// Schema 定义客户端能选字段 type Query { product(id: ID!): Product } type Product { id: ID! name: String! price: BigDecimal! supplier: Supplier // 关联对象可选择性获取 } // DataFetcher每个字段对应一段取数逻辑 DataFetcherProduct productFetcher env - { String id env.getArgument(id); return productService.get(id); // ① 按需取商品 }; DataFetcherSupplier supplierFetcher env - { Product p env.getSource(); // ② 拿到上游对象再取供应商 return supplierService.get(p.getSupplierId()); };前端只要写query { product(id:1){ name price } }就只拿 name 和 price不发 supplier 的请求——这正是 REST 解决不了的 over-fetching。但 GraphQL 引入了一个 REST 没有的坑N1 查询。如果上面supplierFetcher在列表查询里对 100 个商品各查一次供应商就是 100 次 DB 调用。我们用DataLoader做批量缓存才压住BatchLoaderString, Supplier loader ids - supplierService.batchGet(ids).toCompletionStage(); // ① 把 N 次查合并成 1 次 IN 查询 DataLoaderString, Supplier dataLoader DataLoaderFactory.newDataLoader(loader);① 是 GraphQL 性能的生命线没有 DataLoader列表接口在并发下能把数据库打挂。这一点常被忽视很多人上了 GraphQL 才发现列表页比 REST 还慢。我们那次过度设计事情是这样的我们有一个内部运营后台查询量极低日活不到 50 人字段也固定。有同事觉得GraphQL 更先进把整个后台 API 重写成 GraphQL。结果开发效率反而下降每个字段要写 DataFetcher前端每个页面要手搓 queryIDE 补全和报错体验远不如 REST OpenAPI 文档。更糟的是鉴权——GraphQL 的字段级权限要自己实现我们漏配了一个costPrice字段的权限差点把成本价暴露给运营以外的角色靠 code review 拦下来了没真出事但足够惊出一身汗。最后那个后台又改回了 REST。我现在的判断很干脆GraphQL 适合前端形态多变、多端共用一个后端、字段组合爆炸的场景比如面向多客户端的 BFF 层它能显著减少前端的接口对接成本但对于字段固定、客户端单一、对缓存和简单性要求高的内部系统REST 就是更省心的选择。别因为技术新就上 GraphQL它带来的复杂度是实打实的字段权限、查询复杂度限制防止恶意深度查询打垮服务端、N1每一件都要额外处理。错误处理的两种风格选型时还有个容易被忽略的差异是错误处理。REST 靠 HTTP 状态码错误天然可被中间件识别GraphQL 默认所有响应都是 200错误放在errors数组里这意味着你的网关、监控、告警都要专门去解析 body 才能发现失败。我们当时为了 GraphQL 的错误处理单独写了一套错误提取和告警逻辑又是一笔隐性成本。如果你的可观测性体系是围绕 HTTP 状态码建的大部分团队都是换 GraphQL 时要评估这块改造量。一张表看清怎么选维度RESTGraphQL学习/接入成本低工具链成熟较高需 schema、fetcher、loader前端灵活性固定结构易 over/under-fetch客户端按需取字段缓存CDN/浏览器原生支持需自己实现难度高字段级权限按端点控制需逐字段实现适合场景公开 API、内部系统、固定客户端多端 BFF、字段组合多变我的取舍清单对外公开 API、要被第三方和 CDN 缓存的选 REST生态和缓存优势太大。多端 App/Web 共用、界面频繁改版、字段组合多的考虑 GraphQL 做 BFF。内部后台、字段固定、人少REST OpenAPI别折腾。不管选哪个都要有 API 契约测试见上一篇否则接口一变下游全懵。别为了统一技术栈强行全公司只用一种——我们后来是对外和内部后台用 REST面向 C 端的聚合层用 GraphQL按场景分反而最稳。还有第三种选择gRPC 在什么位置把 REST 和 GraphQL 讲完得提一句容易被忽略的第三种选择——gRPC。它不走 HTTP/1.1 JSON而是 HTTP/2 Protobuf强类型、体积小、支持双向流。我们在内部服务间比如订单调库存这种高频、契约稳定、对延迟敏感的场景用 gRPC 1.60 取代了一部分 REST序列化体积比 JSON 小 60%RT 从 8ms 降到 3ms。但 gRPC 天生不友好于浏览器需要 grpc-web 代理也不利于 CDN 缓存所以它和 GraphQL 不冲突——我们的分层是外部/浏览器用 REST 或 GraphQL内部服务间用 gRPC。选型本质是按调用方是谁、要不要缓存、要不要流式三个问题分流而不是全公司只用一种风格。版本与演进别追新追稳定选型定了还有隐性成本版本升级。我们用 Spring 6.0 Spring for GraphQL 1.2.x 跑了一年graphql-java 从 19.x 升到 21.x 时一次小版本改了DataLoader的泛型签名编译没报错但运行时类型推断变了导致一个列表接口偶发返回空。这类框架升级引发的隐性行为变化在 REST 里相对少契约在 HTTP 层足够稳定在 GraphQL 里更频繁因为 schema 和 fetcher 是框架强约束的。我的建议GraphQL 相关依赖升级必须进 E2E 回归别只跑单测如果团队没有专职维护这套的人REST 的无聊反而是优势。技术选型不只是选风格也是选你愿意长期维护的复杂度。落地节奏别一上来就分叉最后给个落地节奏建议新项目先用 REST 把主链路跑通、把可观测性和契约测试地基打好等真的出现前端字段组合爆炸、多端重复对接的痛点再在 BFF 层局部引入 GraphQL不要一上来就双栈并行。我们吃过两个风格同时上、团队一半人不会 GraphQL的亏前三个月效率不升反降。技术债里最贵的一种就是为了时髦而引入、却没人能维护的半成品方案。我们现在的准则是任何新引入的 API 风格必须至少有一名同事能独立排查它的线上问题否则就暂时不上——能排查才配拥有。最后补一句无论选哪种API 的向后兼容都比选型本身更影响长期维护成本。我们吃过一次GraphQL 删了个字段的亏三个老客户端同时报错。所以只要是暴露给别人的接口变更都要走标记废弃 → 观察 → 删除三步别一步到位。选型解决的是今天的问题兼容性纪律决定的是明天的你还要不要为今天的选择买单。思考题你现在的项目是 REST 还是 GraphQL有没有哪个场景你觉得换一种会更舒服欢迎评论区聊聊你的选型踩坑。