Tworzenie bezpiecznej pod względem typów API GraphQL za pomocą Prisma i Nexus w Node.js
Postępuj zgodnie z siedmioetapowym przewodnikiem, aby stworzyć API Node.js GraphQL, które łączy model danych Prisma z typami i rozwiązywaczami wygenerowanymi przez Nexus.
Odkryj, jak wdrożyć Prisma Nexus do projektu Node.js w celu tworzenia bezpiecznych pod względem typów API GraphQL, omawiając projektowanie schematu, logikę rozwiązywania zapytań oraz uruchomiony serwer.
Załóżmy projekt GraphQL, w którym ten sam typ „User” jest zdefiniowany w czterech różnych miejscach: w dokumencie SDL, ręcznie napisanej interfejsie TypeScript, modelu Prisma oraz walidatorze Zod dodanym przez kolegę z zespołu kilka miesięcy po uruchomieniu. Za każdym razem, gdy zmienia się jedna z tych definicji, przynajmniej jedna z pozostałych traci synchronizację. Po wdrożeniu poprawki typy TypeScript nadal zakładają, że pole phone jest obowiązkowe, mimo że kolumna zniknęła z bazy danych już kilka tygodni wcześniej.
Taki rodzaj rozbieżności jest dokładnie tym, czemu ma zapobiec łączenie Prismy z Nexusem. Nexus tworzy twoje schemat GraphQL oraz typy TypeScript bezpośrednio na podstawie tego samego modelu danych, który już zdefiniowałeś w Prismie. Istnieje jedno źródło prawdy, a wszystko inne pochodzi od niego. Wystarczy zaktualizować definicję raz, a typy, schemat oraz sygnatury resolverów zmienią się jednocześnie. Brzmi to jak zdrowy rozsądek, gdy tylko to wypowiesz na głos — prawdziwa lekcja pochodzi z doświadczenia pracy bez niego i zrozumienia, jak kosztowna staje się ta luka.
To przewodnik pokazuje, jak od zera stworzyć API GraphQL w Node.js przy użyciu Prisma i Nexus, podzielone na siedem kroków z kompletnym kodem, bez żadnych pominięć. Pod koniec będziesz miał działający serwer połączony z PostgreSQL — coś, co możesz uruchomić, rozwijać i z czym możesz pracować z pewnością siebie. Ma służyć jako solidna baza pod prawdziwy backend e-commerce w produkcji, a nie jako demo, które zawodzi już po dodaniu drugiego modelu.
Czego potrzebujesz przed pierwszym krokiem
Będziesz potrzebował:
- Zainstalowanego Node.js — pobierz najnowszą wersję LTS z nodejs.org, jeśli jej jeszcze nie masz.
- Dostępnej globalnie CLI Prisma:
npm install -g prisma
- Działającej bazy danych PostgreSQL, do której możesz się połączyć. Może to być lokalny kontener Docker, darmowy plan Supabase lub Railway — nie ma znaczenia, jakie jest hostowanie, pod warunkiem, że masz do dyspozycji ciąg połączenia.
Uwaga dla tych, którzy chcą zastosować to rozwiązanie do istniejącej bazy kodu, a nie do nowego projektu: podczas pierwszej migracji Prisma próbuje pogodzić plik schema.prisma z tym, co już znajduje się w bazie danych. W przypadku skomplikowanego, starszego schematu ten krok może spowodować powstanie dużego i zniechęcającego raportu różnic. Przeczytaj go uważnie przed zastosowaniem i zawsze najpierw przetestuj to w środowisku rozwojowym. Jeśli zaczynasz od czystej kartki, to wszystko jeszcze nie dotyczy ciebie.
Krok 1: Uruchomienie projektu
To najszybszy krok w całym procesie. Stwórz folder i pobierz wszystkie zależności jednocześnie:
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
To pojedyncze polecenie ładuje jednocześnie wszystkie siedem pakietów: środowisko wykonawcze GraphQL, Nexus do tworzenia schematów w pierwszej kolejności kodu, sam Prisma oraz kombinację Apollo/Express, która uruchomi serwer. Instalowanie wszystkiego razem to nie tylko kwestia wygody — pozwala npm rozwiązać wszystkie zależności w jednej operacji, zamiast narażać się na niezgodne wersje mniejsze przy instalowaniu pakietów pojedynczo.
Krok 2: Połączenie Prismy z bazą danych
npx prisma init
Odpowiedz na pytania i wybierz PostgreSQL. Gdy polecenie się zakończy, pojawią się dwa nowe pliki, których wcześniej nie było:
prisma/schema.prisma— tutaj znajduje się model danych.env— tutaj umieszcza się ciąg połączeniaDATABASE_URL, i powinien tam trafić natychmiast
To nie jest przesada. Zanim dotkniesz schematu, zanim uruchomisz migrację, zanim otworzysz cokolwiek innego, umieść swój łańcuch połączenia w pliku .env. Od tego momentu praktycznie każda komenda Prisma próbuje nawiązać połączenie z bazą danych, a błędy, które pojawiają się w przypadku braku lub nieprawidłowej postaci tego łańcucha, są wyjątkowo mało pomocne. Zamiast jasnego komunikatu „nieprawidłowy łańcuch połączenia”, otrzymasz niejasną informację o tym, że klient nie został zainicjowany — i możesz łatwo stracić piętnaście minut na szukanie niewłaściwej przyczyny.
Krok 3: Napisz schemat Prisma — to nie jest twój schemat GraphQL
Jeśli wcześniej pracowałeś z GraphQL, ale nigdy razem z Prismą, unikaj traktowania pliku schema.prisma jako miejsca, w którym projektujesz interfejs API. To nie jest tak. Jest to reprezentacja struktury twojej bazy danych — tabel, kolumn, związków i ograniczeń. Rzeczywisty kształt API jest później wyznaczany na podstawie tego pliku, za pośrednictwem Nexusa. Pamiętaj o tej różnicy, ponieważ zapewnia ona spójność całego modelu mentalnego.
// 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
}
Gdy już napiszesz swój model, uruchom migrację:
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
W tym momencie konfiguracji słuszne jest zastanowienie się, czy Nexus rzeczywiście spełnia swoje zadanie. Nic nie stoi na przeszkodzie w stworzeniu serwera GraphQL bez niego – ręcznie napisz SDL, sam zdefiniuj interfejsy w TypeScript i ręcznie połącz wszystko z rozwiązywaczami. Wiele baz kodu robi dokładnie to. Problem polega na tym, że taki podejście otwiera drogę do określonego rodzaju błędów: SDL wskazuje na jeden kształt, typy w TypeScript opisują nieco inny, a rozwiązywacz zwraca coś zupełnie innego. Zrozumienie, która z tych trzech wersji jest „prawdziwa”, często zajmuje więcej czasu niż samo tworzenie tej funkcjonalności od początku.
Nexus unika tego problemu, traktując SDL jako wynik generowany, a nie coś, co tworzysz ręcznie. Opisujesz swoje typy w TypeScript, a Nexus wyprowadza zarówno SDL, jak i odpowiadające im definicje typów z tego jedynego źródła. Te trzy elementy, które wcześniej mogły się rozchodzić, stają się jednym artefaktem, który strukturalnie nie może być sprzeczny sam ze sobą. Oto jak wygląda 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;
Konfiguracja outputs wskazuje Nexusowi, gdzie umieścić pliki, które generuje: generated/schema.graphql przechowuje definicję SDL, a generated/nexus.ts odpowiadające im definicje w języku TypeScript. Obie te pliki są przepisywane przy każdym uruchomieniu, więc nigdy nie powinieneś ich edytować ręcznie. Jeśli otworzysz generated/nexus.ts i zauważysz coś, co wymaga poprawy, powstrzymaj się od bezpośredniej edycji — zamiast tego znajdź źródłową definicję i zmień ją tam. Modyfikowanie pliku wygenerowanego jest nieco podobne do naprawiania skompilowanego pliku binarnego: działa to dopóki następna kompilacja nie wymaże twoich zmian w tajemnicy.
Krok 5: Rozwiązujące — łączenie schematu z bazą danych
// 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,
},
});
},
});
},
});
Zauważ, że PrismaClient jest tworzony tylko raz, na najwyższym poziomie modułu, poza ciałem jakiejkolwiek funkcji. To umiejscowienie ma większe znaczenie, niż mogłoby się wydawać na pierwszy rzut oka. Każde wezwanie new PrismaClient() otwiera nowe połączenie z bazą danych. Gdyby go tworzyć wewnątrz resolvera, nowe połączenie powstawałoby przy każdej prośbie. Podczas normalnego rozwoju lokalnego, przy jednej lub dwóch prośbach na sekundę, baza danych nawet nie zauważy różnicy. Jednak przy rzeczywistym ruchu równoczesnym — wyobraźmy sobie kilkaset klientów korzystających z /checkout jednocześnie podczas promocji — taki model wyczerpie limit połączeń PostgreSQL i zacznie generować błędy pod obciążeniem.
Deklarowanie klienta na poziomie modułu oznacza, że cały proces korzysta z jednego połączenia. Prośby nie konkurują o otwarcie własnych połączeń do bazy danych; są układane w kolejce dla jednego wspólnego klienta, który wewnętrznie zarządza własnym zbiorem połączeń. To właśnie tego typu szczegóły doświadczeni deweloperzy Node.js stosują automatycznie, podczas gdy mniej doświadczone zespoły zwykle odkrywają to na własnej skórze, w trakcie incydentu. Teraz możesz pominąć tę lekcję.
Krok 6: Serwer
// 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);
});
Jeden szczegół, na który warto zwrócić uwagę przed rozpoczęciem pracy: await server.start() musi zostać wywołany przed server.applyMiddleware(). To wymaganie dotyczące kolejności nie istniało w Apollo Server 2 — Apollo 3 wprowadził wyraźną asynchroniczną fazę uruchamiania, a każdy kod przykładowy napisany przed końcem 2021 roku prawdopodobnie w ogóle nie zawiera tego wywołania. Jeśli go pominiesz, pojawi się błąd Server must be started before calling server.applyMiddleware, który przynajmniej jasno wskazuje, co poszło nie tak, choć nie wyjaśnia, dlaczego obowiązuje to prawило. Gdy zrozumiesz uzasadnienie, naprawa zajmie zaledwie dwie sekundy i nie będzie to kłopotliwy objazd.
Krok 7: Uruchom to. Zepsuj to. Ufaj temu.
node server.ts
Przejdź do adresu http://localhost:4000/graphql. To otworzy GraphQL Playground. Najpierw uruchom mutację:
// Fetch Users
query {
users {
id
name
email
}
}
// Create Users
mutation {
createUser(name: "John Doe", email: "john@example.com") {
id
name
email
}
}
Zainicjuj mutację przed zapytaniem, aby rzeczywiście było co pobierać. Obserwuj, jak rekord, który właśnie dodałeś, pojawia się w odpowiedzi na zapytanie. Następnie zrób coś, czego większość przewodników pomija: otwórz klienta bazy danych — psql, TablePlus, DBeaver lub cokolwiek innego, co masz — i sprawdź bezpośrednio tabelę User. Nie JSON, który zwróciła API, lecz samą tabelę w surowej postaci.
Twoja wiersz znajduje się tam — został utworzony dzięki mutacji GraphQL zdefiniowanej w TypeScript przy użyciu typów Nexus, wykonywanej przez Prisma i zapisanej w PostgreSQL. Każdy ogniwko w tym łańcuchu zadziałało poprawnie. Możesz wskazać dokładne miejsce, gdzie kod twojego aplikacji łączy się z bazą danych. Dla osób przyzwyczajonych do lat pracy z punktami końcowymi REST i ręcznie pisanym SQL to zazwyczaj moment, w którym ten stack przestaje wyglądać jak schemat i zaczyna być czymś rzeczywistym.
To, co zbudowałeś, i to, co nadal musisz dodać
To, co masz teraz, to działająca baza backendu, a nie przykład do ćwiczeń. Schemat, którego właśnie użyłeś — zdefiniowanie modelu Prisma, uruchomienie migracji, dodanie obiektu typu Nexus, napisanie rozwiązania oraz połączenie go z serwerem Apollo/Express — to dokładnie to, co będziesz powtarzać przy każdym nowym modelu. Niezależnie od tego, czy chodzi o Product, Order czy Cart, kroki pozostają takie same, podobnie jak gwarancje. Dodaj relację w pliku schema.prisma, uruchom polecenie migrate dev, a następnie zaimplementuj rozwiązanie — wtedy twoje typy aktualizują się same. Ta automatyczna synchronizacja jest właśnie główną zaletą tego rozwiązania — nie musisz już polegać na pamięci, aby twoje schematy, typy i rozwiązania były spójne, ponieważ narzędzia to za ciebie zapewniają.
To, czego dotychczas wyraźnie brakuje: autoryzacja, uprawnienia, ograniczenie szybkości oraz walidacja danych wejściowych. Nexus gwarantuje, że Twoje typy są poprawne. Nie mówi nic na temat tego, kto ma prawo wykonywać określone operacje. Obecnie mutacja createUser chętnie odpowie każdemu, kto może uzyskać dostęp do portu 4000. Jest to akceptowalne podczas rozwoju lokalnego. Staje się nacechowane problemami w momencie, gdy API staje się dostępne pod rzeczywistą adresem URL. Dodanie warstwy pośredniczącej do autoryzacji musi nastąpić, zanim system trafi choćby w środowisko dostępne dla innych osób.
Aby uzyskać bardziej szczegółowe informacje, sprawdź dokumentację Prisma dotyczącą relacji, filtrowania i paginacji, a także dokumentację Nexus na temat autoryzacji na poziomie pól i niestandardowych skalarnych wartości. Obie te bazy dokumentacji są dobrze zorganizowane, dzięki czemu można je przeczytać od początku do końca, zamiast tylko szybko przeglądać je w razie problemów — co jest cechą rzadszą, niż powinno być w dokumentacji technicznej.
Powiązane materiały
- Tworzenie bezpiecznych agentów AI za pomocą LangChain Guardrails i middleware — Dowiedz się, jak mechanizmy oparte na modelach i deterministyczne zasady działają w LangChain, aby wykrywać wycieki danych osobowych, egzekwować zasady biznesowe oraz dodawać kroki zatwierdzenia przez człowieka do agentów AI.