搬瓦工 VPS 搭建 GraphQL API 服务教程

GraphQL 是由 Facebook 开发的 API 查询语言,允许客户端精确指定需要的数据字段,一次请求获取多个资源,有效避免了 REST API 中常见的过度获取和多次请求问题。本教程将介绍如何在搬瓦工 VPS 上使用 Node.js 和 Apollo Server 搭建生产级 GraphQL API 服务,包括 Schema 设计、数据库集成和 Docker 部署。部署前请确保已安装好 Docker 和 Docker Compose

一、GraphQL 核心概念

  • Schema:定义 API 的数据类型和操作,包括 Query(查询)、Mutation(变更)和 Subscription(订阅)。
  • Resolver:解析器函数,为 Schema 中的每个字段提供数据获取逻辑。
  • Type System:强类型系统,所有数据必须符合预定义的类型。
  • Introspection:自省能力,客户端可以查询 Schema 结构,便于文档生成和开发工具集成。

二、项目初始化

mkdir -p /opt/graphql-api && cd /opt/graphql-api

# 初始化 Node.js 项目
cat > package.json <<'EOF'
{
  "name": "graphql-api-server",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start": "node src/index.js",
    "dev": "node --watch src/index.js"
  },
  "dependencies": {
    "@apollo/server": "^4.10.0",
    "graphql": "^16.8.0",
    "pg": "^8.12.0",
    "dataloader": "^2.2.2",
    "graphql-scalars": "^1.23.0"
  }
}
EOF

三、定义 GraphQL Schema

mkdir -p src
cat > src/schema.js <<'EOF'
export const typeDefs = `#graphql
  scalar DateTime

  type User {
    id: ID!
    name: String!
    email: String!
    age: Int
    posts: [Post!]!
    createdAt: DateTime!
  }

  type Post {
    id: ID!
    title: String!
    content: String!
    published: Boolean!
    author: User!
    comments: [Comment!]!
    createdAt: DateTime!
  }

  type Comment {
    id: ID!
    text: String!
    author: User!
    post: Post!
    createdAt: DateTime!
  }

  type Query {
    user(id: ID!): User
    users(limit: Int, offset: Int): [User!]!
    post(id: ID!): Post
    posts(published: Boolean, limit: Int, offset: Int): [Post!]!
    searchPosts(keyword: String!): [Post!]!
  }

  type Mutation {
    createUser(input: CreateUserInput!): User!
    updateUser(id: ID!, input: UpdateUserInput!): User!
    deleteUser(id: ID!): Boolean!
    createPost(input: CreatePostInput!): Post!
    publishPost(id: ID!): Post!
    addComment(input: AddCommentInput!): Comment!
  }

  input CreateUserInput {
    name: String!
    email: String!
    age: Int
  }

  input UpdateUserInput {
    name: String
    email: String
    age: Int
  }

  input CreatePostInput {
    title: String!
    content: String!
    authorId: ID!
  }

  input AddCommentInput {
    text: String!
    postId: ID!
    authorId: ID!
  }

  type Subscription {
    postPublished: Post!
    commentAdded(postId: ID!): Comment!
  }
`;
EOF

四、实现 Resolver

cat > src/resolvers.js <<'EOF'
export const resolvers = {
  Query: {
    user: async (_, { id }, { db }) => {
      const result = await db.query('SELECT * FROM users WHERE id = $1', [id]);
      return result.rows[0];
    },
    users: async (_, { limit = 20, offset = 0 }, { db }) => {
      const result = await db.query(
        'SELECT * FROM users ORDER BY created_at DESC LIMIT $1 OFFSET $2',
        [limit, offset]
      );
      return result.rows;
    },
    post: async (_, { id }, { db }) => {
      const result = await db.query('SELECT * FROM posts WHERE id = $1', [id]);
      return result.rows[0];
    },
    posts: async (_, { published, limit = 20, offset = 0 }, { db }) => {
      let query = 'SELECT * FROM posts';
      const params = [];
      if (published !== undefined) {
        query += ' WHERE published = $1';
        params.push(published);
      }
      query += ` ORDER BY created_at DESC LIMIT $${params.length + 1} OFFSET $${params.length + 2}`;
      params.push(limit, offset);
      const result = await db.query(query, params);
      return result.rows;
    },
  },
  User: {
    posts: async (parent, _, { loaders }) => {
      return loaders.postsByUser.load(parent.id);
    },
  },
  Post: {
    author: async (parent, _, { loaders }) => {
      return loaders.user.load(parent.author_id);
    },
    comments: async (parent, _, { loaders }) => {
      return loaders.commentsByPost.load(parent.id);
    },
  },
  Mutation: {
    createUser: async (_, { input }, { db }) => {
      const { name, email, age } = input;
      const result = await db.query(
        'INSERT INTO users (name, email, age) VALUES ($1, $2, $3) RETURNING *',
        [name, email, age]
      );
      return result.rows[0];
    },
    createPost: async (_, { input }, { db }) => {
      const { title, content, authorId } = input;
      const result = await db.query(
        'INSERT INTO posts (title, content, author_id) VALUES ($1, $2, $3) RETURNING *',
        [title, content, authorId]
      );
      return result.rows[0];
    },
  },
};
EOF

五、服务入口文件

cat > src/index.js <<'EOF'
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import pg from 'pg';
import { typeDefs } from './schema.js';
import { resolvers } from './resolvers.js';

const pool = new pg.Pool({
  host: process.env.DB_HOST || 'localhost',
  database: process.env.DB_NAME || 'graphql_db',
  user: process.env.DB_USER || 'graphql_user',
  password: process.env.DB_PASS || 'graphql_pass_2026',
  max: 20,
});

const server = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: process.env.NODE_ENV !== 'production',
});

const { url } = await startStandaloneServer(server, {
  listen: { port: parseInt(process.env.PORT || '4000') },
  context: async ({ req }) => ({
    db: pool,
    token: req.headers.authorization,
  }),
});

console.log(`GraphQL server ready at ${url}`);
EOF

六、Docker Compose 部署

cat > Dockerfile <<'EOF'
FROM node:20-alpine
WORKDIR /app
COPY package.json ./
RUN npm install --production
COPY src/ ./src/
EXPOSE 4000
CMD ["node", "src/index.js"]
EOF

cat > docker-compose.yml <<'EOF'
version: '3.8'

services:
  graphql:
    build: .
    container_name: graphql-api
    restart: always
    ports:
      - "127.0.0.1:4000:4000"
    environment:
      NODE_ENV: production
      PORT: "4000"
      DB_HOST: postgres
      DB_NAME: graphql_db
      DB_USER: graphql_user
      DB_PASS: graphql_pass_2026
    depends_on:
      - postgres

  postgres:
    image: postgres:15-alpine
    container_name: graphql-db
    restart: always
    environment:
      POSTGRES_DB: graphql_db
      POSTGRES_USER: graphql_user
      POSTGRES_PASSWORD: graphql_pass_2026
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql

volumes:
  pgdata:
EOF

docker compose up -d

七、查询示例

# 查询用户及其文章
curl -X POST http://localhost:4000/graphql \
  -H 'Content-Type: application/json' \
  -d '{"query": "{ users(limit: 5) { id name email posts { title published } } }"}'

# 创建用户
curl -X POST http://localhost:4000/graphql \
  -H 'Content-Type: application/json' \
  -d '{"query": "mutation { createUser(input: { name: \"Alice\", email: \"alice@example.com\", age: 28 }) { id name } }"}'

八、生产优化

  • DataLoader:使用 DataLoader 解决 N+1 查询问题,批量加载关联数据。
  • 查询深度限制:防止恶意深度嵌套查询消耗过多资源。
  • 持久化查询:预编译查询语句,减少解析开销并提升安全性。
  • 缓存:利用 Apollo Cache 或 Redis 缓存热点查询结果。

九、常见问题

N+1 查询问题

当查询嵌套关联数据时会产生大量数据库查询,使用 DataLoader 进行批量加载可以有效解决此问题。

生产环境禁用 Introspection

在生产环境中应禁用 Schema 自省功能,防止 API 结构被外部探测。设置环境变量 NODE_ENV=production 即可自动禁用。

总结

GraphQL 是构建灵活 API 的现代化方案,特别适合前端驱动的应用开发。配合 Swagger 文档Postman 测试可以建立完整的 API 开发工作流。选购搬瓦工 VPS 请参考 全部方案,购买时使用优惠码 NODESEEK2026 可享受 6.77% 的折扣,购买链接:bwh81.net

关于本站

搬瓦工VPS中文网(bwgvps.com)是非官方中文信息站,整理搬瓦工的方案、优惠和教程。我们不销售主机,不提供技术服务。

新手必读
搬瓦工优惠码

NODESEEK2026(优惠 6.77%)

购买时填入即可抵扣。