Создание безопасного с точки зрения типов GraphQL API с Prisma и Nexus в Node.js
Следуйте семиступенчатой инструкции для создания GraphQL API на Node.js, которое объединяет модель данных Prisma с типами и резолверами, генерируемыми Nexus.
Узнайте, как внедрить Prisma Nexus в проект на Node.js для создания безопасных с точки зрения типов GraphQL-API, рассмотрев проектирование схемы, логику обработчиков запросов и запуск сервера.
Представьте себе проект на GraphQL, в котором один и тот же тип «User» определяется в четырех разных местах: в документе SDL, в ручно написанном интерфейсе TypeScript, в модели Prisma и в фильтре проверки Zod, добавленном коллегой спустя несколько месяцев после запуска. Каждый раз, когда меняется одно из этих определений, хотя бы одно из остальных теряет синхронизацию. Выпускается исправление, но типы TypeScript по-прежнему считают поле phone обязательным, хотя оно исчезло из базы данных еще несколько недель назад.
Именно такое отклонение и предназначено предотвратить сочетание Prisma с Nexus. Nexus создаёт вашу схему GraphQL и типы TypeScript непосредственно на основе той же модели данных, которую вы уже определили в Prisma. Существует единый источник истины, от которого зависит всё остальное. Достаточно обновить определение один раз, и типы, схема и подписи резолверов автоматически обновятся вместе с ним. Это звучит как здравый смысл, когда вы это говорите вслух — настоящий урок заключается в том, чтобы работать без этого подхода и почувствовать, насколько дорого обходится такой недостаток.
В этом руководстве пошагово описывается процесс создания GraphQL API на Node.js с нуля с использованием Prisma и Nexus; процесс разделен на семь шагов, приводятся полные примеры кода без пропусков. К концу вы получите рабочий сервер, подключенный к PostgreSQL — что-то, что можно запустить, расширить и использовать с уверенностью. Это руководство предназначено как прочная основа для настоящего производственного фронтенда электронной коммерции, а не демо-версия, которая разрушается сразу после добавления второй модели.
Что нужно подготовить перед первым шагом
Вам понадобится:
- Установленный Node.js — скачайте текущую LTS-версию с nodejs.org, если у вас ее еще нет.
- CLI Prisma, доступная глобально:
npm install -g prisma
- Работающая база данных PostgreSQL, к которой можно получить доступ. Это может быть локальный контейнер Docker, бесплатный тариф Supabase или Railway — формат хостинга не имеет значения, главное иметь под рукой строку подключения.
Примечание для тех, кто планирует применять это к существующей базе кода, а не к новому проекту: во время первой миграции Prisma пытается согласовать файл schema.prisma с тем, что уже находится в базе данных. При наличии сложной устаревшей схемы этот шаг может привести к появлению большого и запутанного отчета о различиях. Внимательно изучите его перед применением и всегда сначала тестируйте в разработочной среде. Если вы начинаете с чистого листа, эти правила пока к вам не относятся.
Шаг 1: Запуск проекта
Это самый быстрый шаг во всем процессе. Создайте папку и сразу загрузите все зависимости:
mkdir prisma-nexus-graphql
cd prisma-nexus-graphql
# Initialize your project
npm init -y# Install required dependencies
npm install graphql nexus prisma express apollo-server-express path
Эта одна команда загружает все семь пакетов сразу: движок GraphQL, Nexus для создания схемы на основе кода, сам Prisma, а также комплект Apollo/Express для запуска сервера. Установка всего вместе — это не просто удобство: npm может решить вопрос взаимозависимостей между всеми пакетами за один проход, в отличие от ситуации, когда при установке пакетов по одному возникает риск несоответствия версий минорных компонентов.
Шаг 2: Подключение Prisma к вашей базе данных
npx prisma init
Ответьте на вопросы и выберите PostgreSQL. После завершения команды появятся два новых файла, которых раньше не было:
prisma/schema.prisma— здесь хранится модель данных.env— сюда должна быть помещена строка подключенияDATABASE_URL, и её следует поместить сразу же
Это не преувеличение. Прежде чем взаимодействовать со схемой, прежде чем запустить миграцию или что-либо ещё, сохраните строку подключения в файл .env. С этого момента практически каждая команда Prisma будет пытаться подключиться к базе данных, и ошибки, возникающие при отсутствии или некорректной форме строки, бывают крайне неудобными для диагностики. Вместо чёткого сообщения «некорректная строка подключения» вы получите расплывчатую информацию о том, что клиент не инициализирован — и вам придётся тратить пятнадцать минут на поиски неверной причины проблемы.
Шаг 3: Написание схемы Prisma — это не ваша GraphQL-схема
Если вы раньше работали с GraphQL, но никогда не использовали Prisma вместе с ним, избегайте рассмотрения файла schema.prisma как места для проектирования интерфейса API. Это не так. Этот файл представляет собой структуру вашей базы данных — таблицы, столбцы, связи, ограничения. Фактическая структура API формируется позже с помощью Nexus на основе этой информации. Помните об этом различии, поскольку оно помогает сохранять целостность всей когнитивной модели.
// schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}model User {
id Int @id @default(autoincrement())
name String
email String @unique
}
После того как модель будет написана, запустите миграцию:
npx prisma migrate dev
This single command does two things nothing else in the setup does: it creates the actual table in your database, and it regenerates Prisma Client with TypeScript types that exactly match your current schema. Skip it, and Prisma Client simply won't recognize that a User model exists. What you get instead are type errors buried in generated files you don't control, with call stacks that lead nowhere useful — there's no clever shortcut around that. Run the migration every time your schema changes, without exception.
Step 4: Nexus — Why One More File Is Worth It
На этом этапе настройки вполне уместно задаться вопросом: действительно ли Nexus выполняет свою роль. Ничто не мешает создать сервер GraphQL без него — вручную написать SDL, самостоятельно определить интерфейсы на TypeScript и вручную связать всё с резолверами. Множество проектов делают именно это. Однако такой подход открывает путь к определённому типу ошибок: SDL указывает на одну форму, типы TypeScript описывают немного другую, а резолвер возвращает совершенно другой результат. Выяснение того, какая из трёх версий является «настоящей», часто занимает больше времени, чем само создание функционала изначально.
Nexus решает эту проблему, рассматривая SDL как генерируемый результат, а не как что-то, что создаётся вручную. Вы описываете свои типы на TypeScript, и Nexus выводит как SDL, так и соответствующие определения типов из этого единственного источника. Три компонента, которые раньше могли расходиться, объединяются в один продукт, структурно не способный противоречить самому себе. Вот как выглядит schema.ts:
// schema.ts
import { makeSchema } from 'nexus';
import path from 'path';
import * as resolvers from './resolvers';const schema = makeSchema({
types: [resolvers],
outputs: {
schema: path.join(__dirname, './generated/schema.graphql'),
typegen: path.join(__dirname, './generated/nexus.ts'),
},
});export default schema;
Конфигурация outputs указывает Nexus, куда сохранять генерируемые им файлы: generated/schema.graphql содержит SDL, а generated/nexus.ts — соответствующие определения на TypeScript. Оба файла переписываются при каждой сборке, поэтому их ни в коем случае не следует редактировать вручную. Если вы откроете generated/nexus.ts и заметите что-то, что требует исправления, сдерживайте желание изменить его напрямую — вместо этого найдите исходное определение и измените его там. Изменение генерируемого файла похоже на внесение патча в скомпилированный бинарник: такой подход работает до следующей сборки, когда ваши изменения молча удаляются.
Шаг 5: Резолверы — связь схемы с базой данных
// resolvers.ts
import { extendType, stringArg, nonNull, objectType } from 'nexus';
import { PrismaClient } from '@prisma/client';const prisma = new PrismaClient();export const User = objectType({
name: 'User',
definition(t) {
t.nonNull.id('id')
t.string('name')
t.string('email')
},
})export const Query = extendType({
type: 'Query',
definition(t) {
t.list.field('users', {
type: 'User',
resolve: async () => {
return await prisma.user.findMany();
},
});
},
});export const Mutation = extendType({
type: 'Mutation',
definition(t) {
t.field('createUser', {
type: 'User',
args: {
name: nonNull(stringArg()),
email: nonNull(stringArg()),
},
resolve: async (_, args) => {
return await prisma.user.create({
data: {
name: args.name,
email: args.email,
},
});
},
});
},
});
Обратите внимание, что объект PrismaClient создаётся только один раз, на верхнем уровне модуля, вне тела какой-либо функции. Такое расположение имеет большее значение, чем может показаться на первый взгляд. Каждый вызов new PrismaClient() открывает новое соединение с базой данных. Если бы его создавали внутри резолвера, новое соединение формировалось бы при каждом запросе. Во время обычной локальной разработки, когда поступает один-два запроса в секунду, база данных даже не заметит разницы. Однако при реальном одновременном трафике — представьте, что сотни покупателей одновременно обращаются к адресу /checkout во время распродажи — такой подход исчерпает лимит соединений PostgreSQL, из-за чего при высокой нагрузке начнут появляться ошибки.
Указание клиента на уровне модуля означает, что весь процесс использует одно соединение. Запросы не соревнуются за открытие собственных базовых данных — они формируют очередь к одному общему клиенту, который внутренне управляет собственным пулом соединений. Именно такие детали опытные разработчики Node.js применяют автоматически, тогда как менее опытные команды обычно узнают об этом на собственном опыте, во время инцидентов. Теперь вам не придётся проходить через это.
Шаг 6: Сервер
// server.ts
import express from 'express';
import { ApolloServer } from 'apollo-server-express';
import schema from './schema';const app = express();
const server = new ApolloServer({ schema });const startServer = async () => {
await server.start(); // Start Apollo Server server.applyMiddleware({ app }); // Apply Apollo Server middleware to Express const PORT = process.env.PORT || 4000; app.listen(PORT, () => {
console.log(`Server is running at http://localhost:${PORT}/graphql`);
});
}startServer().catch((err) => {
console.error('Error starting the server:', err);
});
Один момент, на который стоит обратить внимание перед тем, как начать работу: команда await server.start() должна выполняться раньше, чем server.applyMiddleware(). Такое требование к порядку выполнения отсутствовало в Apollo Server 2 — Apollo 3 ввёл явную асинхронную фазу запуска, и любой пример кода, написанный до конца 2021 года, скорее всего, совсем не содержит этого вызова. Если его пропустить, появится ошибка Server must be started before calling server.applyMiddleware, которая, по крайней мере, чётко указывает на проблему, хотя и не объясняет причину существования этого правила. Как только вы поймёте логику, её исправление займёт всего две секунды, а не будет сложным объяснением.
Шаг 7: Запустите его. Нарушьте его работу. Доверьтесь ему.
node server.ts
Перейдите по адресу http://localhost:4000/graphql. Это откроет GraphQL Playground. Сначала выполните мьютацию:
// Fetch Users
query {
users {
id
name
email
}
}
// Create Users
mutation {
createUser(name: "John Doe", email: "john@example.com") {
id
name
email
}
}
Выполните мутацию до запроса, чтобы действительно были данные для загрузки. Посмотрите, как запись, которую вы только что вставили, появляется в ответе на запрос. Затем сделайте то, чего обычно не делают в учебных примерах: откройте клиент базы данных — psql, TablePlus, DBeaver или любой другой инструмент — и непосредственно изучите таблицу User. Не JSON, возвращаемый API, а саму сырую таблицу.
Ваша строка там есть — она была создана с помощью мутации GraphQL, определенной в TypeScript с использованием типов Nexus, выполнена через Prisma и сохранена в PostgreSQL. Все звенья этой цепочки сработали. Вы можете указать точное место, где код вашего приложения взаимодействует с базой данных. Для тех, кто много лет работал с REST-эндпоинтами и вручную писал SQL, обычно именно в этот момент такая стек-технология перестает казаться просто схемой и начинает восприниматься как реальный инструмент.
Что вы уже создали и что еще нужно добавить
У вас сейчас есть рабочая основа бэкенда, а не просто демонстрационный пример. Паттерн, который вы только что использовали — определение модели Prisma, выполнение миграции, добавление объекта типа Nexus, написание резолвера и его подключение к серверу Apollo/Express — именно тот, который будет использоваться для каждой новой модели. Будь то Product, Order или Cart, шаги остаются прежними, как и гарантии. Добавьте связь в файл schema.prisma, выполните команду migrate dev, затем реализуйте резолвер — и ваши типы автоматически обновятся. Именно такая автоматическая синхронизация является ключевым преимуществом этой конфигурации: вам больше не нужно полагаться на память для согласования схемы, типов и резолверов, поскольку инструменты сами этим занимаются.
Что пока явно отсутствует: аутентификация, авторизация, ограничение частоты запросов и проверка входных данных. Nexus гарантирует, что типы данных верны. Он ничего не говорит о том, кому разрешено выполнять ту или иную операцию. В текущем виде мутация createUser с радостью отвечает любому, кто может подключиться по порту 4000. Это приемлемо во время локальной разработки. Однако это перестает быть приемлемым, как только API становится доступен по реальному URL. Добавление промежуточного слоя аутентификации необходимо до того, как система попадет в среду, к которой могут иметь доступ другие люди.
Для более подробной информации ознакомьтесь с документацией Prisma по связям между объектами, фильтрации и пагинации, а также с документацией Nexus по авторизации на уровне полей и пользовательских скалярам. Обе серии документации хорошо структурированы, что позволяет прочитать их целиком, а не лишь бегло сканировать в поисках решения при возникновении проблем — что является редкостью среди технической документации.
Связанные материалы
- Создание безопасных ИИ-агентов с использованием LangChain Guardrails и middleware — Узнайте, как функционируют детерминистичные и основанные на моделях механизмы контроля в LangChain для выявления утечек персональных данных, обеспечения соблюдения бизнес-правил и добавления этапов утверждения действий ИИ-агентами.