Головна / Статті / Створення безпечного за типами GraphQL API з Prisma та Nexus у Node.js

Створення безпечного за типами GraphQL API з Prisma та Nexus у Node.js

Дотримуйтесь семикрокової інструкції для створення API Node.js GraphQL, яке об’єднує модель даних Prisma з типами та резолверами, створеними Nexus.

2244 слів

Дізнайтеся, як інтегрувати Prisma Nexus у проект Node.js для створення безпечних за типами GraphQL API, розглянувши проектування схеми, логіку обробки запитів та функціонування сервера.

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 objectType, написання резолвера та його підключення до сервера Apollo/Express — саме так ви будете діяти для кожної нової моделі. Чи то Product, Order чи Cart, кроки залишаються незмінними, як і гарантії. Додайте зв’язок у schema.prisma, запустіть migrate dev, потім реалізуйте резолвер, і ваші типи оновляться автоматично. Саме ця автоматична синхронізація є основною перевагою такої конфігурації — вам більше не потрібно покладатися на пам’ять для збереження узгодженості схеми, типів та резолверів, адже інструменти забезпечують це за вас.

Що наразі помітно відсутнє: автентифікація, авторизація, обмеження частоти запитів та перевірка вхідних даних. Nexus гарантує, що ваші типи даней правильні. Він нічого не каже про те, кому дозволено виконувати певні операції. У поточному вигляді мутація createUser буде охоче відповідати будь-кому, хто може достучатися до порту 4000. Це прийнятно під час розробки локально. Однак це вже не є прийнятним, як тільки API стає доступним за справжньою URL. Додавання проміжного сервісу для автентифікації є необхідним, перш ніж це потрапить у середовище, до якого можуть мати доступ інші люди.

Для більш детального ознайомлення перегляньте документацію Prisma щодо взаємозв’язків, фільтрації та сторінкування, а також документацію Nexus про авторизацію на рівні полів та користувацькі скалари. Обидві набори документації належним чином організовані, що дозволяє прочитати їх від початку до кінця, а не лише швидко переглядати у разі проблем — що є рідкістю серед технічної документації.

Пов’язані матеріали

  • Заміна Jest на вбудований тест-раннер Node у Node 24 — Приклад реальної міграції показує, як вбудований тест-раннер Node 24 та нативна підтримка TypeScript скорочують час виконання тестів у середовищі CI, водночас усуваючи чотири залежності.
  • Усунення помилки відсутності бібліотеки libssl.so.1.1 у Prisma в Alpine Docker — Дізнайтеся, чому двигун запитів Prisma зупиняється у образах Docker на базі Alpine через помилку відсутності libssl, та як назавжди її вирішити.
  • Міграція від Prisma до Drizzle: огляд за шість місяців — Розробник ділиться реальними показниками та компромісами, пов’язаними зі зміною стеку PostgreSQL та TypeScript від Prisma на Drizzle ORM.