GraphQL vs REST API - Kiedy stosować i co wybrać?
GraphQL REST API
porownaniaGraphQL vs REST API - Kompleksowe Porównanie Podejść do Budowy API
Wybór odpowiedniej architektury API to jedna z fundamentalnych decyzji w każdym projekcie webowym. Przez ponad dekadę REST (Representational State Transfer) był niekwestionowanym standardem budowy interfejsów programistycznych. W 2015 roku Facebook udostępnił publicznie GraphQL - język zapytań do API, który miał rozwiązać problemy, z jakimi borykali się przy rozwijaniu swojej aplikacji mobilnej. Dziś oba podejścia mają swoje miejsce w ekosystemie - ale kiedy wybrać które? W tym artykule szczegółowo porównamy GraphQL i REST API pod każdym kątem.
Czym jest REST API?#
REST to styl architektoniczny oparty na zasobach. Każdy zasób (np. użytkownik, produkt, zamówienie) ma swój unikalny URL, a operacje na nim wykonuje się za pomocą standardowych metod HTTP.
Podstawowe zasady REST#
- Zasoby identyfikowane przez URL -
/api/users/1,/api/products/42 - Metody HTTP jako operacje - GET (odczyt), POST (tworzenie), PUT/PATCH (aktualizacja), DELETE (usunięcie)
- Bezstanowość - każde żądanie zawiera wszystkie informacje potrzebne do jego przetworzenia
- Jednolity interfejs - spójne konwencje dla wszystkich endpointów
- Warstwa cache - odpowiedzi mogą być buforowane na podstawie nagłówków HTTP
Przykład REST API#
// Serwer Express.js - REST API
import express from 'express';
const app = express();
app.use(express.json());
// GET /api/users - lista użytkowników
app.get('/api/users', async (req, res) => {
const users = await db.user.findMany();
res.json(users);
});
// GET /api/users/:id - szczegóły użytkownika
app.get('/api/users/:id', async (req, res) => {
const user = await db.user.findUnique({
where: { id: Number(req.params.id) },
include: { posts: true, profile: true },
});
if (!user) return res.status(404).json({ error: 'Nie znaleziono' });
res.json(user);
});
// POST /api/users - tworzenie użytkownika
app.post('/api/users', async (req, res) => {
const user = await db.user.create({ data: req.body });
res.status(201).json(user);
});
// PUT /api/users/:id - aktualizacja użytkownika
app.put('/api/users/:id', async (req, res) => {
const user = await db.user.update({
where: { id: Number(req.params.id) },
data: req.body,
});
res.json(user);
});
// DELETE /api/users/:id - usunięcie użytkownika
app.delete('/api/users/:id', async (req, res) => {
await db.user.delete({ where: { id: Number(req.params.id) } });
res.status(204).send();
});
Czym jest GraphQL?#
GraphQL to język zapytań i środowisko uruchomieniowe dla API, które pozwala klientowi precyzyjnie określić, jakie dane potrzebuje. Zamiast wielu endpointów, mamy jeden punkt wejścia i elastyczny system zapytań oparty na schemacie typów.
Podstawowe koncepcje GraphQL#
- Schemat i typy - kontrakt między klientem a serwerem, definiujący kształt danych
- Zapytania (Queries) - odczyt danych z precyzyjnym wyborem pól
- Mutacje (Mutations) - operacje modyfikujące dane
- Subskrypcje (Subscriptions) - dane w czasie rzeczywistym przez WebSocket
- Resolvery - funkcje definiujące sposób pozyskiwania danych dla każdego pola
Przykład GraphQL API#
// Schemat GraphQL
const typeDefs = `#graphql
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
profile: Profile
}
type Post {
id: ID!
title: String!
content: String!
author: User!
createdAt: String!
}
type Profile {
id: ID!
bio: String
avatar: String
}
type Query {
users: [User!]!
user(id: ID!): User
posts(limit: Int, offset: Int): [Post!]!
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
}
input CreateUserInput {
name: String!
email: String!
password: String!
}
input UpdateUserInput {
name: String
email: String
}
`;
// Resolvery
const resolvers = {
Query: {
users: () => db.user.findMany(),
user: (_, { id }) => db.user.findUnique({ where: { id } }),
posts: (_, { limit, offset }) =>
db.post.findMany({ take: limit, skip: offset }),
},
Mutation: {
createUser: (_, { input }) => db.user.create({ data: input }),
updateUser: (_, { id, input }) =>
db.user.update({ where: { id }, data: input }),
deleteUser: async (_, { id }) => {
await db.user.delete({ where: { id } });
return true;
},
},
User: {
posts: (parent) =>
db.post.findMany({ where: { authorId: parent.id } }),
profile: (parent) =>
db.profile.findUnique({ where: { userId: parent.id } }),
},
};
Problem Over-fetching i Under-fetching#
To prawdopodobnie najczęściej przywoływany argument na rzecz GraphQL.
Over-fetching w REST#
Over-fetching występuje, gdy endpoint zwraca więcej danych, niż klient potrzebuje.
// REST: GET /api/users/1
// Odpowiedź zawiera WSZYSTKIE pola, nawet jeśli potrzebujesz tylko imienia
{
"id": 1,
"name": "Jan Kowalski",
"email": "jan@example.com",
"phone": "+48 123 456 789",
"address": "ul. Przykładowa 1, Warszawa",
"bio": "Lorem ipsum dolor sit amet...",
"avatar": "/uploads/jan.webp",
"createdAt": "2024-01-15T10:30:00Z",
"lastLogin": "2024-03-01T08:15:00Z",
"preferences": { "theme": "dark", "language": "pl" }
}
Under-fetching w REST#
Under-fetching to sytuacja, gdy jeden endpoint nie dostarcza wystarczających danych i potrzeba kilku żądań.
// Potrzebujesz: użytkownik + jego posty + komentarze do postów
// REST wymaga trzech żądań:
const user = await fetch('/api/users/1');
const posts = await fetch('/api/users/1/posts');
const comments = await fetch('/api/posts/42/comments');
Rozwiązanie GraphQL#
# Jedno zapytanie - dokładnie te dane, które potrzebujesz
query {
user(id: "1") {
name
posts {
title
comments {
text
author {
name
}
}
}
}
}
GraphQL pozwala pobrać dokładnie te pola i relacje, które są potrzebne, w jednym żądaniu. To ogromna zaleta szczególnie na urządzeniach mobilnych z ograniczoną przepustowością.
Schemat i Typowanie#
GraphQL - Schema-first vs Code-first#
GraphQL wymusza definiowanie schematu typów, co tworzy kontrakt między frontendem a backendem.
// Code-first z @nestjs/graphql
@ObjectType()
export class User {
@Field(() => ID)
id: string;
@Field()
name: string;
@Field()
email: string;
@Field(() => [Post])
posts: Post[];
}
@InputType()
export class CreateUserInput {
@Field()
@IsNotEmpty()
name: string;
@Field()
@IsEmail()
email: string;
}
Schema GraphQL stanowi jednocześnie dokumentację i walidację - klient wie dokładnie, jakie typy danych może oczekiwać.
REST - OpenAPI/Swagger#
REST nie ma wbudowanego systemu typów, ale można go uzupełnić specyfikacją OpenAPI.
# openapi.yaml
paths:
/api/users/{id}:
get:
summary: Pobierz użytkownika
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/User'
components:
schemas:
User:
type: object
properties:
id:
type: integer
name:
type: string
email:
type: string
format: email
W REST schemat typów jest opcjonalny i wymaga dodatkowego narzędzia, co w praktyce bywa pomijane.
Wersjonowanie API#
REST - Wersjonowanie w URL lub Nagłówkach#
// Wersjonowanie w URL
app.get('/api/v1/users', handleUsersV1);
app.get('/api/v2/users', handleUsersV2);
// Wersjonowanie w nagłówkach
app.get('/api/users', (req, res) => {
const version = req.headers['api-version'] || '1';
if (version === '2') return handleUsersV2(req, res);
return handleUsersV1(req, res);
});
Wersjonowanie REST API jest proste koncepcyjnie, ale kosztowne w utrzymaniu - każda nowa wersja to potencjalna duplikacja kodu i konieczność wsparcia starszych wersji.
GraphQL - Ewolucja bez Wersji#
type User {
id: ID!
name: String!
email: String!
# Nowe pole - stare klienty po prostu go nie pobierają
phoneNumber: String
# Oznaczenie pola jako przestarzałe
phone: String @deprecated(reason: "Użyj phoneNumber")
}
GraphQL nie potrzebuje wersjonowania. Dodajesz nowe pola, a stare oznaczasz jako @deprecated. Klienty pobierają tylko to, co potrzebują, więc dodawanie pól nie łamie kompatybilności wstecznej.
Cachowanie#
REST - Naturalne Cachowanie HTTP#
REST świetnie współpracuje z warstwami cache HTTP, ponieważ każdy zasób ma unikalny URL.
// Nagłówki cache w REST
app.get('/api/products/:id', (req, res) => {
res.set({
'Cache-Control': 'public, max-age=3600',
'ETag': `"product-${product.id}-${product.updatedAt}"`,
});
res.json(product);
});
// CDN, przeglądarki i proxy automatycznie buforują odpowiedzi
// GET /api/products/42 -> cache HIT (ten sam URL = ten sam zasób)
GraphQL - Cachowanie Wymaga Dodatkowej Pracy#
Ponieważ GraphQL używa jednego endpointu i zapytań POST, tradycyjne cache HTTP nie działają.
// Apollo Client - normalizowany cache po stronie klienta
const client = new ApolloClient({
cache: new InMemoryCache({
typePolicies: {
User: {
keyFields: ['id'],
},
Post: {
keyFields: ['id'],
fields: {
comments: {
merge(existing = [], incoming) {
return [...existing, ...incoming];
},
},
},
},
},
}),
});
// Persisted Queries - konwersja POST na GET z hashem
// POST { query: "{ user(id: 1) { name } }" }
// staje się:
// GET /graphql?extensions={"persistedQuery":{"sha256Hash":"abc123..."}}
Apollo Client oferuje zaawansowany normalizowany cache, ale wymaga to konfiguracji. REST wygrywa pod względem prostoty cachowania.
Obsługa Błędów#
REST - Kody Statusu HTTP#
// REST - standardowe kody HTTP
app.get('/api/users/:id', async (req, res) => {
try {
const user = await db.user.findUnique({
where: { id: Number(req.params.id) },
});
if (!user) {
return res.status(404).json({
error: 'NOT_FOUND',
message: 'Użytkownik nie istnieje',
});
}
res.json(user);
} catch (error) {
res.status(500).json({
error: 'INTERNAL_ERROR',
message: 'Wewnętrzny błąd serwera',
});
}
});
// Klient: status 404 = zasób nie istnieje, 401 = brak autoryzacji, itd.
GraphQL - Błędy w Ciele Odpowiedzi#
// GraphQL zawsze zwraca status 200
{
"data": {
"user": null
},
"errors": [
{
"message": "Użytkownik nie istnieje",
"locations": [{ "line": 2, "column": 3 }],
"path": ["user"],
"extensions": {
"code": "NOT_FOUND",
"statusCode": 404
}
}
]
}
// Obsługa błędów w resolverach
import { GraphQLError } from 'graphql';
const resolvers = {
Query: {
user: async (_, { id }) => {
const user = await db.user.findUnique({ where: { id } });
if (!user) {
throw new GraphQLError('Użytkownik nie istnieje', {
extensions: { code: 'NOT_FOUND', statusCode: 404 },
});
}
return user;
},
},
};
W GraphQL odpowiedź prawie zawsze ma status 200, a błędy przekazywane są w polu errors. Może to być mylące dla narzędzi monitorujących, które opierają się na kodach HTTP.
Problem N+1#
Problem N+1 jest szczególnie istotny w kontekście GraphQL, ponieważ klient może zażądać zagnieżdżonych relacji, co generuje wiele zapytań do bazy danych.
Problem#
# Zapytanie GraphQL
query {
users { # 1 zapytanie SQL: SELECT * FROM users
name
posts { # N zapytań SQL: SELECT * FROM posts WHERE author_id = ?
title # (jedno zapytanie na każdego użytkownika)
}
}
}
Jeśli mamy 100 użytkowników, wykonamy 101 zapytań do bazy (1 + 100).
Rozwiązanie - DataLoader#
import DataLoader from 'dataloader';
// DataLoader grupuje zapytania i wykonuje je wsadowo
const postLoader = new DataLoader(async (userIds: string[]) => {
const posts = await db.post.findMany({
where: { authorId: { in: userIds } },
});
// Mapowanie wyników do odpowiednich userIds
const postsByUser = userIds.map((id) =>
posts.filter((post) => post.authorId === id)
);
return postsByUser;
});
// W resolverze
const resolvers = {
User: {
posts: (parent) => postLoader.load(parent.id),
},
};
// Teraz zamiast 101 zapytań mamy tylko 2:
// SELECT * FROM users
// SELECT * FROM posts WHERE author_id IN (1, 2, 3, ..., 100)
DataLoader to standardowe rozwiązanie problemu N+1 w GraphQL. W REST problem ten jest mniej dotkliwy, bo to serwer kontroluje zapytania do bazy - klient nie decyduje o głębokości zagnieżdżenia.
Subskrypcje - Dane w Czasie Rzeczywistym#
GraphQL Subscriptions#
// Schemat
const typeDefs = `#graphql
type Subscription {
messageAdded(channelId: ID!): Message!
userStatusChanged: UserStatus!
}
type Message {
id: ID!
text: String!
author: User!
createdAt: String!
}
`;
// Resolver subskrypcji
const resolvers = {
Subscription: {
messageAdded: {
subscribe: (_, { channelId }) =>
pubsub.asyncIterator([`MESSAGE_ADDED_${channelId}`]),
},
},
Mutation: {
sendMessage: async (_, { channelId, text }, { user }) => {
const message = await db.message.create({
data: { text, channelId, authorId: user.id },
});
pubsub.publish(`MESSAGE_ADDED_${channelId}`, {
messageAdded: message,
});
return message;
},
},
};
// Klient - subskrypcja w React z Apollo
const MESSAGES_SUBSCRIPTION = gql`
subscription OnMessageAdded($channelId: ID!) {
messageAdded(channelId: $channelId) {
id
text
author {
name
avatar
}
createdAt
}
}
`;
function ChatRoom({ channelId }) {
const { data, loading } = useSubscription(MESSAGES_SUBSCRIPTION, {
variables: { channelId },
});
// Nowa wiadomość pojawia się automatycznie
return <MessageList messages={data?.messageAdded} />;
}
Subskrypcje to natywna funkcja GraphQL, której REST nie oferuje bez dodatkowych mechanizmów (WebSocket, Server-Sent Events).
Narzędzia i Ekosystem#
GraphQL#
| Narzędzie | Opis | |-----------|------| | Apollo Client | Najpopularniejszy klient GraphQL z cache i zarządzaniem stanem | | Apollo Server | Produkcyjny serwer GraphQL dla Node.js | | Relay | Klient GraphQL od Meta, zoptymalizowany pod wydajność | | GraphQL Code Generator | Automatyczne generowanie typów TypeScript ze schematu | | GraphiQL / Apollo Studio | Interaktywne IDE do eksploracji API | | Hasura | Automatyczny GraphQL API z bazy danych PostgreSQL | | Prisma | ORM z natywnym wsparciem dla generowania schematu GraphQL |
REST#
| Narzędzie | Opis | |-----------|------| | Postman | Najpopularniejsze narzędzie do testowania API | | Swagger/OpenAPI | Standard dokumentowania REST API | | Insomnia | Lekka alternatywa dla Postmana | | cURL | Uniwersalne narzędzie wiersza poleceń | | Hoppscotch | Open-source'owe narzędzie do testowania API w przeglądarce | | Express.js / Fastify | Popularne frameworki HTTP | | json-server | Szybki mock serwer REST z pliku JSON |
Ekosystem REST jest bardziej dojrzały i szerszy. GraphQL nadrabia zaległości, a narzędzia takie jak Apollo i GraphQL Code Generator znacząco podnoszą Developer Experience.
Wydajność#
REST - Przewidywalna Wydajność#
W REST każdy endpoint ma zdefiniowaną złożoność. Serwer kontroluje, jakie dane są zwracane, co ułatwia optymalizację.
// REST - łatwa optymalizacja per endpoint
app.get('/api/products', async (req, res) => {
const products = await db.product.findMany({
select: { id: true, name: true, price: true, thumbnail: true },
take: 20,
});
res.json(products);
});
GraphQL - Elastyczność Kosztem Kontroli#
W GraphQL klient decyduje o głębokości zapytania, co może prowadzić do złożonych zapytań obciążających serwer.
// Ochrona przed złożonymi zapytaniami
import depthLimit from 'graphql-depth-limit';
import { createComplexityLimitRule } from 'graphql-validation-complexity';
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
depthLimit(5), // Maksymalna głębokość zagnieżdżenia
createComplexityLimitRule(1000), // Limit złożoności
],
});
GraphQL wymaga dodatkowych mechanizmów ochrony - limitowania głębokości zapytań, analizy złożoności i rate limitingu per zapytanie (nie per endpoint, jak w REST).
Kiedy Wybrać REST API?#
- Proste operacje CRUD - REST naturalnie mapuje się na operacje bazodanowe
- Intensywne cachowanie - cache HTTP działa natywnie z REST
- Publiczne API - REST jest prostszy do zrozumienia dla zewnętrznych deweloperów
- Przesyłanie plików - REST obsługuje multipart/form-data bez dodatkowej konfiguracji
- Mikroserwisy z prostymi kontraktami - REST jest lżejszy i mniej złożony
- Zespół bez doświadczenia z GraphQL - krzywa uczenia REST jest łagodniejsza
- API z niewielką liczbą klientów - gdy over-fetching nie jest problemem
Kiedy Wybrać GraphQL?#
- Wiele klientów o różnych potrzebach - mobilne, webowe, IoT - każdy pobiera inne dane
- Złożone relacje między danymi - zagnieżdżone zapytania eliminują wiele żądań HTTP
- Szybko ewoluujące API - dodawanie pól bez wersjonowania
- Aplikacje real-time - wbudowane subskrypcje
- Agregacja wielu serwisów - GraphQL jako API Gateway (Federation)
- Developer Experience - autouzupełnianie, typowanie, interaktywna dokumentacja
- Aplikacje mobilne - minimalizacja transferu danych jest krytyczna
Podejście Hybrydowe#
W praktyce wiele zespołów łączy oba podejścia, wykorzystując ich najsilniejsze strony.
┌─────────────────────┐
│ API Gateway │
│ (GraphQL) │
└──────┬──────────────┘
│
┌──────────────┼──────────────┐
│ │ │
┌─────▼────┐ ┌─────▼────┐ ┌─────▼────┐
│ User │ │ Product │ │ Order │
│ Service │ │ Service │ │ Service │
│ (REST) │ │ (REST) │ │ (REST) │
└──────────┘ └──────────┘ └──────────┘
// GraphQL jako warstwa agregacji REST mikroserwisów
const resolvers = {
Query: {
orderDetails: async (_, { orderId }) => {
const [order, user, products] = await Promise.all([
fetch(`http://orders-service/api/orders/${orderId}`).then(r => r.json()),
fetch(`http://users-service/api/users/${order.userId}`).then(r => r.json()),
fetch(`http://products-service/api/products?ids=${order.productIds.join(',')}`).then(r => r.json()),
]);
return { ...order, user, products };
},
},
};
Popularnym wzorcem jest GraphQL Federation (Apollo Federation), gdzie każdy mikroserwis może definiować swoją część schematu GraphQL, a gateway łączy je w jeden spójny schemat.
Tabela Porównawcza#
| Aspekt | REST | GraphQL | |--------|------|---------| | Architektura | Wiele endpointów, zasoby | Jeden endpoint, zapytania | | Pobieranie danych | Stałe odpowiedzi per endpoint | Klient wybiera dokładne pola | | Over-fetching | Częste | Brak | | Under-fetching | Częste (wiele żądań) | Brak (jedno zapytanie) | | Typowanie | Opcjonalne (OpenAPI) | Wbudowane (Schema) | | Wersjonowanie | URL lub nagłówki | Ewolucja schematu | | Cachowanie | Natywne HTTP cache | Wymaga Apollo/Relay | | Błędy | Kody statusu HTTP | Pole errors w odpowiedzi | | Real-time | SSE/WebSocket (dodatkowe) | Wbudowane subskrypcje | | Przesyłanie plików | Natywne multipart | Wymaga dodatkowej konfiguracji | | Krzywa uczenia | Niska | Średnia-wysoka | | Narzędzia | Postman, cURL, Swagger | Apollo Studio, GraphiQL | | Wydajność | Przewidywalna | Wymaga ochrony przed złożonością |
Podsumowanie#
Nie ma jednoznacznej odpowiedzi na pytanie "GraphQL czy REST?". Oba podejścia mają swoje silne strony i zastosowania. REST pozostaje doskonałym wyborem dla prostych, dobrze zdefiniowanych API z intensywnym cachowaniem i publicznym dostępem. GraphQL sprawdza się najlepiej w złożonych aplikacjach z wieloma klientami, gdzie elastyczność pobierania danych i silne typowanie mają kluczowe znaczenie.
Trend rynkowy wskazuje na rosnącą adopcję GraphQL, szczególnie w aplikacjach frontendowych i mobilnych. Jednocześnie REST nie odchodzi do lamusa - obie technologie będą współistnieć, a najskuteczniejsze zespoły potrafią wykorzystać obie tam, gdzie każda się sprawdza najlepiej.
Potrzebujesz Profesjonalnego API dla Swojego Projektu?#
W MDS Software Solutions Group projektujemy i wdrażamy wydajne API zarówno w architekturze REST, jak i GraphQL. Nasz zespół pomoże Ci wybrać optymalne podejście, zaprojektować schemat danych i zbudować skalowalne rozwiązanie dopasowane do potrzeb Twojego biznesu.
Skontaktuj się z nami, aby omówić Twój projekt - od analizy wymagań i konsultacji architektonicznej po pełne wdrożenie i utrzymanie API.
Zespół ekspertów programistycznych specjalizujących się w nowoczesnych technologiach webowych.