Галоўная / Артыкулы / Стварэнне безпечнай за типамі 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, разглянуўшы проектаванне схемы, логіку рашэння запитаў і працуючы сервер.

Уявіце сабе проект на GraphQL, дзе той самы тип „User“ ўвесь час визначаецца у чатырох разных месцах: у дакументе SDL, у ручна напісанай інтэрфейсе на TypeScript, у модэлі Prisma і ў верыфікаторе Zod, які колега дадаў месцы пасля запуску. Кожны раз, калі змянюецца адна з гэтых визначэнняў, прынеймна адна з іншых стае несінхронной. Выпускаецца патч, і раптам типы на TypeScript яшчэ прыпускаюць, што паказнік phone є обавязковым, хоця гэты столбец у базе дадзеных з’явіўся ўжо калісьце раней.

Такі проблемы якраз і мае на мету запобiec супрацоўка Prisma з Nexus. Nexus стварае вашу схему GraphQL і типы TypeScript безпосередна на адной і той жа модэлі дадзеных, якую вы вялікі ў Prisma. Існуе едынай адказгоднае джэрела інфармацыі, і всё, што выкарыстоўваецца пасля таго, адбываецца на ўснове яго. Дыктуйчы змяну адной раз, типы, схема і падписы рэшараў таксама зміняюцца. Гэта звучыць як здаровы глузд, калі вы гэта выгаворваеце — але справжні урок крыўця ў тым, калі працуеце без гэтага і відчуваеце, насколькі дорога становіцца гэтая праслойка.

Этыя інструкцыі паказвают, як з нуля стварыць API на базе Node.js і GraphQL за дапамою Prisma i 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

Эты ўказны каманд выпрацоўвае два дзелы, больш нічога не рабячы ў процэсе наладкі: ён стварае саму таблицу ў вашай базе дадзенаў і перзаскладвае Prisma Client з типамі на TypeScript, якія абоўсумова падходзяць да вашага чыннага схематызавання. Якщо яго праігнораваць, Prisma Client проста не будзе ведаць, што існуе модель User. У такім случае вы паказваеце бяговыя памылкі, якія знаходзяцца ў створаных файлах, якімі вы не керуеце, а таксама стэкі вызываў, якія не вядуць нікуды корыстна — адсутня жадная хитрая альтернатыва. Заўсёды, без выключэння, запускайце міграцыю кожны раз, калі змянюецца ваш схематызавання.

Шаг 4: Nexus — чаму ўсё-такі варта дадаць ўтолькі файлу

У гэты момент практычнай наладкі цялкам правядзьвае запытанне: чы рэальна 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 пра автарызацыю на рэвэлі поляў і спецыяльныя скэляры. Бацькі документы арганізаваны настолькі добра, што іх можна прачытаць ад пачатку да канца, а не толькі шукаць рашэнне, калі ўтворыцца проблема — што яўляецца рэдкім якостяй у тэхнічных документах.

Спадзяючыся матэрыялы

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