后端搭建 GraphQL 服务器
约 596 字大约 2 分钟
GraphQLApollo ServerNode.js
2026-04-08
本文以 Apollo Server 4 + Node.js 为例,完整演示如何从零搭建一个 GraphQL 服务。
1. 初始化项目
mkdir graphql-server && cd graphql-server
pnpm init
pnpm add @apollo/server graphql graphql-tag
pnpm add -D typescript @types/node tsx
npx tsc --init2. 定义 Schema
src/schema.ts:
import gql from 'graphql-tag'
export const typeDefs = gql`
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
}
type Query {
users: [User!]!
user(id: ID!): User
posts: [Post!]!
}
input CreatePostInput {
title: String!
content: String!
authorId: ID!
}
type Mutation {
createPost(input: CreatePostInput!): Post!
}
`3. 模拟数据源
src/data.ts:
export const users = [
{ id: '1', name: 'ZhenYu', email: 'zhenyu@example.com' },
{ id: '2', name: 'Alice', email: 'alice@example.com' },
]
export const posts = [
{ id: '1', title: 'Hello GraphQL', content: '...', authorId: '1' },
{ id: '2', title: 'Apollo Server 入门', content: '...', authorId: '1' },
]4. 编写 Resolvers
src/resolvers.ts:
import { users, posts } from './data'
export const resolvers = {
Query: {
users: () => users,
user: (_: unknown, args: { id: string }) =>
users.find((u) => u.id === args.id),
posts: () => posts,
},
Mutation: {
createPost: (_: unknown, args: { input: any }) => {
const newPost = {
id: String(posts.length + 1),
...args.input,
}
posts.push(newPost)
return newPost
},
},
User: {
posts: (parent: { id: string }) =>
posts.filter((p) => p.authorId === parent.id),
},
Post: {
author: (parent: { authorId: string }) =>
users.find((u) => u.id === parent.authorId),
},
}注意 User.posts 和 Post.author 是字段级 resolver,GraphQL 引擎会在查询到这些字段时自动调用。
5. 启动服务
src/index.ts:
import { ApolloServer } from '@apollo/server'
import { startStandaloneServer } from '@apollo/server/standalone'
import { typeDefs } from './schema'
import { resolvers } from './resolvers'
interface Context {
token?: string
}
const server = new ApolloServer<Context>({
typeDefs,
resolvers,
})
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => ({
token: req.headers.authorization,
}),
listen: { port: 4000 },
})
console.log(`🚀 Server ready at ${url}`)运行:
pnpm tsx src/index.ts访问 http://localhost:4000 即可进入 Apollo Sandbox 调试界面。
6. N+1 问题与 DataLoader
上述代码中,如果一次查询返回 100 个用户且每个用户都要取 posts,User.posts resolver 会被调用 100 次,每次都查一次数据库 —— 这就是 N+1 问题。
解决方案:使用 DataLoader 做批量合并和缓存。
pnpm add dataloaderimport DataLoader from 'dataloader'
const createPostsByUserLoader = () =>
new DataLoader(async (userIds: readonly string[]) => {
const rows = await db.posts.findByUserIds(userIds as string[])
// 按 userIds 顺序对齐
return userIds.map((id) => rows.filter((r) => r.authorId === id))
})
// 每次请求创建一个新的 loader 实例,挂在 context 上
const server = new ApolloServer({
typeDefs,
resolvers,
})
await startStandaloneServer(server, {
context: async () => ({
loaders: {
postsByUser: createPostsByUserLoader(),
},
}),
})在 resolver 中使用:
User: {
posts: (parent, _args, context) =>
context.loaders.postsByUser.load(parent.id),
}7. 鉴权与错误
推荐在 context 中完成身份解析,在 resolver 中判断权限;使用 GraphQLError 抛出业务错误:
import { GraphQLError } from 'graphql'
if (!context.user) {
throw new GraphQLError('Unauthorized', {
extensions: { code: 'UNAUTHENTICATED', http: { status: 401 } },
})
}8. 与 HTTP 框架集成
除了 startStandaloneServer,Apollo Server 也可挂载到 Express、Fastify、Koa 等框架:
import express from 'express'
import { expressMiddleware } from '@apollo/server/express4'
const app = express()
await server.start()
app.use('/graphql', express.json(), expressMiddleware(server))
app.listen(4000)