Собираем сервер на Apollo Server 4

Apollo Server 4 — популярный способ поднять GraphQL-сервер на Node.js. Нужны всего две вещи: схема (typeDefs) и резолверы — остальное библиотека берёт на себя.

Схема плюс резолверы равно сервер. Apollo Server 4 убирает весь обвес и оставляет только эти две сущности — и работающий эндпоинт.

Мы разобрали схему и резолверы по отдельности — теперь соберём их в живой сервер. Самый распространённый инструмент в экосистеме Node.js — Apollo Server 4. Сначала ставим пакеты:

npm install @apollo/server graphql

Дальше нужны две части. typeDefs — схема на SDL в виде строки. resolvers — карта функций, которую мы изучили в прошлом уроке. Передаём обе в конструктор ApolloServer:

import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";

// 1. Схема
const typeDefs = `#graphql
  type Book { title: String!, author: String! }
  type Query { books: [Book!]! }
`;

// 2. Данные + резолверы
const books = [
  { title: "Чистый код", author: "Мартин" },
  { title: "Грокаем алгоритмы", author: "Бхаргава" },
];

const resolvers = {
  Query: { books: () => books },
};

// 3. Сервер
const server = new ApolloServer({ typeDefs, resolvers });

const { url } = await startStandaloneServer(server, {
  listen: { port: 4000 },
});

console.log("Сервер готов на " + url);

Запускаем — и на http://localhost:4000 открывается интерактивная песочница (Apollo Sandbox), где можно писать запросы и сразу видеть ответы. Это и есть весь минимальный сервер.

Что нового в версии 4

Apollo Server 4 — это переработка предыдущих версий. Ключевые отличия, которые стоит знать: единый пакет @apollo/server вместо россыпи старых; функция startStandaloneServer для быстрого старта без ручной настройки Express; и обязательно — context создаётся на каждый запрос функцией context. Именно сюда кладут текущего пользователя и загрузчики данных:

const { url } = await startStandaloneServer(server, {
  context: async ({ req }) => {
    const user = await getUserFromToken(req.headers.authorization);
    return { user, db };       // попадёт в третий аргумент резолверов
  },
});

Как работает под капотом

При старте Apollo берёт typeDefs, парсит SDL и строит исполняемую схему, привязывая к ней резолверы. На каждый HTTP-запрос он: вызывает функцию context (создаёт свежий контекст), парсит и валидирует запрос по схеме, выполняет резолверы и сериализует результат в конверт { data, errors }. Смоделируем мини-«движок Apollo» на JS:

function makeServer(resolvers) {
  return {
    execute(operation, field, args, makeContext) {
      const context = makeContext();           // свежий контекст на запрос
      const fn = resolvers[operation][field];
      try {
        return { data: { [field]: fn(null, args, context) } };
      } catch (e) {
        return { data: { [field]: null }, errors: [{ message: e.message }] };
      }
    }
  };
}

const server = makeServer({
  Query: { books: (_p, _a, ctx) => ctx.db.books }
});

const res = server.execute("Query", "books", {},
  () => ({ db: { books: ["Чистый код", "Грокаем алгоритмы"] } }));

console.log(JSON.stringify(res));

Попробуй сам ▶ — выброси ошибку внутри резолвера (throw new Error("нет доступа")) и увидишь, как она окажется в errors, а поле станет null. Ровно это делает Apollo.

Частые ошибки

  • Глобальный context. Контекст создаётся на каждый запрос неспроста — туда кладут per-request вещи (пользователь, загрузчики). Глобальный shared-объект приведёт к утечкам данных между запросами.
  • Несоответствие схемы и резолверов. Поле есть в typeDefs, но нет данных/резолвера — получишь null или ошибку. Держи их синхронно.
  • Оставить introspection и Sandbox в проде. Удобно при разработке, но в продакшене это раскрывает всю схему — об этом в уроке про безопасность.

Best practices

  • Держи схему в отдельных .graphql-файлах, а резолверы — по модулям, повторяющим типы; так сервер растёт без хаоса.
  • Всё «на запрос» (пользователь, БД-клиент, DataLoader) создавай в функции context, а не в глобальной области.
  • Для продакшена подключай Apollo к Express/Fastify и закрывай Sandbox/introspection, а не используй standalone «как есть».

Итоги

Apollo Server 4 поднимает GraphQL-сервер из двух частей: typeDefs (схема на SDL) и resolvers (карта функций). startStandaloneServer даёт быстрый старт и Sandbox, а функция context создаёт свежий контекст на каждый запрос — туда кладут пользователя и загрузчики. Дальше используем context, чтобы решить, кому что показывать — авторизация в резолверах.

Проверьте себя
1. Какие две обязательные части нужны Apollo Server 4?
AURL и порт
BtypeDefs (схема) и resolvers (функции)
CБаза данных и кэш
DExpress и WebSocket
2. Почему context в Apollo Server 4 создаётся на каждый запрос?
AДля скорости
BЧтобы класть туда per-request данные (пользователь, загрузчики) и не смешивать запросы
CЭто требование graphql-пакета
DЧтобы кэшировать схему