Przejdź do treści
Porównania

GraphQL vs REST API - Kiedy stosować i co wybrać?

Opublikowano:
·
Zaktualizowano:
·7 min czytania·Autor: MDS Software Solutions Group

GraphQL REST API

porownania

GraphQL 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.

Autor
MDS Software Solutions Group

Zespół ekspertów programistycznych specjalizujących się w nowoczesnych technologiach webowych.

GraphQL vs REST API - Kiedy stosować i co wybrać? | MDS Software Solutions Group | MDS Software Solutions Group