GraphQL 简介与快速上手
🎯 引言
如果你写过前后端联调,一定熟悉这样的场景:页面上只需要用户的名字和头像,但 REST 接口把整个用户对象都返回了回来,一大半字段根本用不上。或者反过来,一个页面要调三四个接口,才能把数据凑齐。
GraphQL 就是为解决这类问题而生的一种 API 方案。学完这篇文章,你能说清楚 GraphQL 是什么、它和 REST 有什么区别,并用 Apollo Server 在本地跑起一个 GraphQL 服务,在网页调试工具里发出第一条查询。这是后续所有文章的基础环境。
🧱 什么是 GraphQL
GraphQL 是一种用于 API 的查询语言,同时是一套服务端运行时。它由 Meta(原 Facebook)开发并在 2015 年开源,如今在前端和 Node.js 社区都有广泛的应用。
它的核心思想可以用一句话概括:客户端声明自己要什么字段,服务端就精确地返回什么字段,不多也不少。
打个比方,REST 接口像餐厅的固定套餐:菜单上写好有什么就给什么,想少要一道菜或者多加点东西都不行。GraphQL 则像自助点餐:你勾选好要的菜,厨房按单子出餐,分量刚好。
一条 GraphQL 查询长这样:
{
users {
username
age
}
}
返回的结果和查询的结构几乎一模一样:
{
"data": {
"users": [
{ "username": "小明", "age": 20 },
{ "username": "小美", "age": 22 }
]
}
}
想要哪个字段就写哪个字段,这就是 GraphQL 的基本工作方式。
⚖️ 和 REST 对比
先花一分钟认识一下 REST。REST 是一种设计 API 的风格,不是协议也不是某个库,核心约定就两条:
- 用 URL 表示资源:把数据看作一种种资源,各有地址,比如
/users表示用户列表、/users/1表示 id 为 1 的用户。 - 用 HTTP 方法表示动作:同一个 URL,用
GET查询、POST新增、PATCH更新、DELETE删除。
之前 Express 课程里写的 app.get('/users', ...),其实就是在搭 REST 接口。它是过去很多年里的主流做法,至今仍被广泛使用。刚接触 GraphQL 时,可以和它对照着理解:
| 对比点 | REST | GraphQL |
|---|---|---|
| 接口入口 | 多个 URL(/users、/articles……) | 通常只有一个入口(如 /graphql) |
| 返回结构 | 服务端定死,客户端被动接受 | 客户端按需声明字段 |
| 取关联数据 | 常常需要多次请求拼数据 | 一次查询嵌套取回 |
| 接口文档 | 需要额外维护(如 Swagger) | Schema 本身就是文档,可在线浏览 |
举个具体例子:页面上要展示「用户列表,每个用户带上他发的文章标题」。用 REST 的话,可能要先请求 /users,再按用户 id 逐个请求文章接口。用 GraphQL 只要一条查询:
{
users {
username
articles {
title
}
}
}
🛠 搭建第一个 GraphQL 服务
GraphQL 本身只是一套规范,需要一个服务端库来落地。Node.js 生态里常用的选择是 Apollo Server,它封装好了请求解析、校验、错误处理等工作,我们只需要写「数据长什么样」和「数据从哪里来」。
本课程基于 Node.js v22 LTS、@apollo/server 5.x、graphql 16.x 编写和验证。新建一个项目目录,初始化后安装:
mkdir graphql-demo && cd graphql-demo
npm init -y
npm pkg set type=module
npm install @apollo/server graphql
npm pkg set type=module:把项目设为 ESM 模式,Apollo Server 5 要求使用 ESM 的import语法。graphql是 GraphQL 规范的官方 JavaScript 实现,Apollo Server 依赖它工作。
然后新建 server.js,写入下面的代码:
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
// 模拟的用户数据
const users = [
{ id: '1', username: '小明', age: 20, city: '北京' },
{ id: '2', username: '小美', age: 22, city: '深圳' },
];
// 1. 用 GraphQL 语言描述数据的结构(Schema)
const typeDefs = `#graphql
type User {
id: ID!
username: String!
age: Int
city: String
}
type Query {
users: [User!]!
user(id: ID!): User
}
`;
// 2. 用 JavaScript 函数提供真实的数据(Resolver)
const resolvers = {
Query: {
users: () => users,
user: (parent, args) => users.find((item) => item.id === args.id),
},
};
// 3. 创建服务并启动
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
});
console.log(`服务已启动:${url}`);
这段代码是 GraphQL 服务端的标准三件套:
- typeDefs(Schema):用 GraphQL 自己的语言描述「有哪些数据、每个字段是什么类型」。
type Query里声明的是客户端可以发起的查询。 - resolvers(解析器):一组 JavaScript 函数,和 typeDefs 里的字段一一对应,负责真正取数据。这里是查内存数组,以后换成查数据库也一样。
- ApolloServer:把两者组装成服务,
startStandaloneServer让它在 4000 端口跑起来。
执行 node server.js,看到 服务已启动:http://localhost:4000/ 就说明服务跑起来了。
✨ 发出第一条查询
Apollo Server 自带一个网页版调试工具 Apollo Sandbox。用浏览器打开 http://localhost:4000/,就能进入它的操作界面:左边写查询,右边看结果。
在左侧输入:
{
users {
username
city
}
}
点击运行按钮,右侧就会返回对应的 JSON 数据。注意我们只写了 username 和 city,返回里就没有 age,这就是「按需取字段」的直观体现。
再试试带参数的查询,按 id 查单个用户:
{
user(id: "1") {
username
age
}
}
还可以故意写一个 Schema 里不存在的字段,比如 hobbies:
{
users {
hobbies
}
}
这次返回的不是数据,而是错误信息:Cannot query field "hobbies" on type "User"。GraphQL 会在执行前先拿查询和 Schema 做校验,字段写错了立刻就能发现,不用等到运行时出问题,这也是 Schema 带来的好处之一。
🧾 小节总结
- GraphQL 是一种 API 查询语言,核心思想是客户端按需声明字段,服务端精确返回。
- 和 REST 对比:GraphQL 通常是单入口、一次查询可以嵌套取关联数据;REST 多接口、返回结构由服务端定死。
- Apollo Server 是 Node.js 生态常用的 GraphQL 服务端库,5.x 版本要求项目使用 ESM。
- 服务端三件套:
typeDefs描述数据结构,resolvers提供真实数据,ApolloServer组装并启动。 - Apollo Sandbox 是自带的网页调试工具,浏览器访问服务地址即可使用。
- GraphQL 会先用 Schema 校验查询,写了不存在的字段会直接报错提示。
❓ 知识问答
Q1:typeDefs 外面包的 #graphql 是什么?
它是一个普通注释,作用是让编辑器把模板字符串里的内容识别为 GraphQL 语言,从而获得语法高亮和提示。删掉也不影响运行,但建议保留。
Q2:为什么查询的最外层可以什么都不写?
{ users { ... } } 其实是 query { users { ... } } 的简写。query 表示这是一次「查询操作」,是最常用的操作类型,可以省略。后面讲修改数据时会用到另一种操作 mutation。
Q3:数据为什么写死在数组里?真实项目怎么办?
学习阶段用内存数组最直观。resolver 只是一个普通函数,里面换成查 MySQL、MongoDB 或请求其他接口都可以,对客户端完全透明。
Q4:GraphQL 是前端技术还是后端技术?
两边都涉及。后端用 GraphQL 定义 Schema、提供数据(本课程的重点),前端按 GraphQL 语法写查询、消费数据(最后一篇文章会讲)。
Q5:端口 4000 被占用了怎么办?
把 listen: { port: 4000 } 改成其他端口(如 4001)即可,浏览器访问的地址也要跟着改。
🧪 小练习
练习一:给 users 数组再加一条你自己的数据,重启服务后,在 Sandbox 里只查出所有用户的 username,不写其他字段。
# 请在这里编写代码
练习二:模仿 user(id: ID!): User 的写法,思考如果要按用户名查询,typeDefs 和 resolvers 分别要加什么,动手试一试。
// 请在这里编写代码
🎉 恭喜你已经跑起了第一个 GraphQL 服务,并理解了「按需取字段」的核心思想!下一篇我们深入 Schema 的类型系统,学会描述更复杂的数据结构。
