Fiszki Online GraphQL (Preview)
Darmowy podgląd 15 z 38 dostępnych pytań
GraphQL - Podstawy i Koncepcje
Czym jest GraphQL i jakie problemy rozwiązuje w porównaniu do REST API?
Odpowiedź w 30 sekund: GraphQL to język zapytań dla API oraz runtime do wykonywania tych zapytań, stworzony przez Facebook w 2012 roku. Rozwiązuje problemy REST API takie jak over-fetching (pobieranie zbędnych danych), under-fetching (konieczność wielu requestów), oraz brak elastyczności w strukturze odpowiedzi.
Odpowiedź w 2 minuty: GraphQL to alternatywa dla REST API, która pozwala klientom precyzyjnie określić, jakie dane chcą pobrać w jednym zapytaniu. W przeciwieństwie do REST, gdzie każdy endpoint zwraca ustaloną strukturę danych, GraphQL umożliwia komponowanie zapytań według potrzeb klienta.
Główne problemy REST, które rozwiązuje GraphQL:
- Over-fetching - w REST często pobieramy więcej danych niż potrzebujemy, bo endpoint zwraca całą strukturę
- Under-fetching - potrzeba wykonania wielu requestów do różnych endpointów, aby zebrać wszystkie potrzebne dane
- Wersjonowanie API - GraphQL pozwala ewoluować API bez konieczności tworzenia nowych wersji
- Dokumentacja - schemat GraphQL jest self-documenting i można go automatycznie eksplorować
GraphQL używa jednego endpointu (zazwyczaj /graphql) i silnego typowania, co ułatwia development i debugging. Klient dokładnie wie, jakie dane otrzyma, a serwer może walidować zapytania przed wykonaniem.
Przykład kodu:
# REST - trzeba wykonać wiele requestów
# GET /users/123
# GET /users/123/posts
# GET /users/123/followers
# GraphQL - jeden request, dokładnie to czego potrzebujesz
query {
user(id: "123") {
name
email
posts {
title
createdAt
}
followers {
name
}
}
}
# Odpowiedź zawiera tylko te pola, które zażądaliśmy
graph LR
A[Klient] -->|Jedno zapytanie| B[GraphQL API]
B --> C[User Service]
B --> D[Post Service]
B --> E[Follower Service]
B -->|Jedna odpowiedź| A
style B fill:#e535ab
Materiały
↑ Powrót na góręJakie są trzy główne operacje w GraphQL (Query, Mutation, Subscription)?
Odpowiedź w 30 sekund: GraphQL definiuje trzy typy operacji: Query (odczyt danych), Mutation (modyfikacja danych - tworzenie, aktualizacja, usuwanie), oraz Subscription (real-time subskrypcje zmian). Query i Mutation są synchroniczne, podczas gdy Subscription wykorzystuje WebSockety do asynchronicznej komunikacji.
Odpowiedź w 2 minuty: GraphQL operacje odpowiadają operacjom CRUD, ale w bardziej semantyczny sposób:
Query - służy do pobierania danych. Jest to operacja read-only, która nie powinna modyfikować stanu na serwerze. Queries mogą być wykonywane równolegle i są cachowalne. Podobnie jak GET w REST.
Mutation - służy do modyfikacji danych (tworzenie, aktualizacja, usuwanie). Mutations są wykonywane sekwencyjnie (jedna po drugiej) w kolejności, w jakiej zostały zdefiniowane w zapytaniu. Odpowiada POST, PUT, PATCH, DELETE w REST.
Subscription - służy do real-time komunikacji. Klient subskrybuje konkretne wydarzenia i otrzymuje aktualizacje przez WebSocket, gdy dane się zmieniają. Idealnie nadaje się do czatów, notyfikacji, live dashboardów. REST nie ma natywnego odpowiednika.
Wszystkie trzy operacje są definiowane w schemacie jako root types: Query, Mutation, i Subscription. Query jest wymagany, pozostałe opcjonalne.
Przykład kodu:
# QUERY - pobieranie danych
query GetUserProfile {
user(id: "123") {
name
email
posts {
title
}
}
}
# MUTATION - modyfikacja danych
mutation CreatePost {
createPost(input: {
title: "Mój nowy post"
content: "Treść posta..."
authorId: "123"
}) {
id
title
createdAt
author {
name
}
}
}
# SUBSCRIPTION - real-time aktualizacje
subscription OnNewPost {
postCreated {
id
title
author {
name
}
createdAt
}
}
# Wiele mutations wykonywanych sekwencyjnie
mutation MultipleOperations {
first: createPost(input: {...}) { id }
second: updateUser(id: "123", input: {...}) { id }
third: deleteComment(id: "456") { success }
}
# Wykonają się po kolei: first → second → third
sequenceDiagram
participant C as Klient
participant S as Serwer GraphQL
Note over C,S: Query - odczyt
C->>S: query { user(id: "123") }
S-->>C: { data: { user: {...} } }
Note over C,S: Mutation - zapis
C->>S: mutation { createPost(...) }
S->>S: Modyfikuje bazę danych
S-->>C: { data: { createPost: {...} } }
Note over C,S: Subscription - real-time
C->>S: subscription { postCreated }
Note over C,S: WebSocket połączenie
S-->>C: { data: { postCreated: {...} } }
S-->>C: { data: { postCreated: {...} } }
Materiały
↑ Powrót na góręCzym jest schemat GraphQL i jak definiuje kontrakt API?
Odpowiedź w 30 sekund: Schemat GraphQL to kontrakt między klientem a serwerem, definiujący wszystkie dostępne typy danych, operacje (queries, mutations, subscriptions) oraz relacje między nimi. Jest napisany w Schema Definition Language (SDL) i stanowi single source of truth dla całego API.
Odpowiedź w 2 minuty: Schemat GraphQL to typowany, self-documenting kontrakt API. Definiuje dokładnie, jakie dane można zapytać, jakie operacje są dostępne i jaką strukturę będą miały odpowiedzi. Schemat używa systemu typów, który jest walidowany podczas kompilacji i wykonania.
Główne elementy schematu:
- Typy obiektów - definiują strukturę danych (np. User, Post, Comment)
- Pola - właściwości obiektów z określonym typem
- Root types - Query, Mutation, Subscription - punkty wejścia do API
- Skalary - podstawowe typy (String, Int, Float, Boolean, ID)
- Enums - wyliczenia możliwych wartości
- Interfaces i Unions - abstrakcje dla polimorfizmu
- Input types - typy używane jako argumenty w mutations
Schemat zapewnia:
- Type safety - błędy typowania wykrywane przed wykonaniem
- Automatyczną dokumentację - narzędzia jak GraphiQL generują docs ze schematu
- Validację - każde zapytanie jest walidowane względem schematu
- Introspection - schemat można odpytać programatowo
Schemat jest niezależny od implementacji - ten sam schemat może być zaimplementowany w różnych językach programowania.
Przykład kodu:
# Definicja schematu w SDL (Schema Definition Language)
# Typy skalarne (wbudowane)
scalar DateTime
# Typ obiektowy
type User {
id: ID! # ! oznacza że pole jest wymagane (non-nullable)
name: String!
email: String!
age: Int
posts: [Post!]! # Tablica postów (nie może być null, elementy nie mogą być null)
role: UserRole!
createdAt: DateTime!
}
# Enum - zdefiniowany zestaw wartości
enum UserRole {
ADMIN
MODERATOR
USER
GUEST
}
type Post {
id: ID!
title: String!
content: String!
author: User! # Relacja do User
comments: [Comment!]!
published: Boolean!
}
type Comment {
id: ID!
text: String!
author: User!
post: Post!
}
# Input type - używany jako argument w mutations
input CreatePostInput {
title: String!
content: String!
authorId: ID!
published: Boolean = false # Wartość domyślna
}
# Root type Query - definiuje dostępne zapytania
type Query {
user(id: ID!): User
users(limit: Int = 10, role: UserRole): [User!]!
post(id: ID!): Post
posts(authorId: ID, published: Boolean): [Post!]!
}
# Root type Mutation - definiuje dostępne modyfikacje
type Mutation {
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: CreatePostInput!): Post!
deletePost(id: ID!): Boolean!
createUser(name: String!, email: String!, role: UserRole!): User!
}
# Root type Subscription - definiuje real-time subskrypcje
type Subscription {
postCreated: Post!
commentAdded(postId: ID!): Comment!
}
# Interface - wspólny kontrakt dla różnych typów
interface Node {
id: ID!
createdAt: DateTime!
}
# Union - typ może być jednym z wielu typów
union SearchResult = User | Post | Comment
graph TD
A[Schemat GraphQL] --> B[Root Types]
A --> C[Object Types]
A --> D[Scalars & Enums]
A --> E[Input Types]
B --> B1[Query]
B --> B2[Mutation]
B --> B3[Subscription]
C --> C1[User]
C --> C2[Post]
C --> C3[Comment]
C1 -.relacja.-> C2
C2 -.relacja.-> C3
C3 -.relacja.-> C1
style A fill:#e535ab
style B fill:#4fb3d9
Materiały
↑ Powrót na góręJak działa introspection w GraphQL i do czego służy?
Odpowiedź w 30 sekund:
Introspection to mechanizm pozwalający odpytać API GraphQL o jego własny schemat. Umożliwia to automatyczne generowanie dokumentacji, type-safety w klientach, oraz tworzenie narzędzi developerskich jak GraphiQL czy Apollo Studio. Jest dostępny przez specjalne zapytania do __schema i __type.
Odpowiedź w 2 minuty:
Introspection to jedna z najbardziej potężnych funkcji GraphQL - możliwość programowego zapytania API o jego strukturę, dostępne typy, pola, oraz operacje. Jest to zaimplementowane jako część standardu GraphQL i działa przez specjalne pola rozpoczynające się od __ (podwójne podkreślenie).
Główne zastosowania introspection:
- Automatyczna dokumentacja - narzędzia jak GraphiQL, GraphQL Playground, Apollo Studio wykorzystują introspection do generowania interaktywnej dokumentacji
- Type generation - generowanie typów TypeScript/Flow na podstawie schematu
- Walidacja zapytań - IDE i editory mogą walidować zapytania w czasie pisania
- Autocomplete - inteligentne podpowiedzi podczas pisania zapytań
- Schema stitching - łączenie wielu schematów GraphQL
Kluczowe pola introspection:
__schema- zwraca cały schemat z wszystkimi typami__type(name: String!)- zwraca informacje o konkretnym typie__typename- zwraca nazwę typu dla danego obiektu (dostępne w każdym zapytaniu)
Uwaga bezpieczeństwa: W środowisku produkcyjnym introspection często jest wyłączony dla publicznych API, aby nie ujawniać pełnej struktury schematu potencjalnym atakującym.
Przykład kodu:
# Pobranie wszystkich typów w schemacie
query IntrospectionQuery {
__schema {
types {
name
kind
description
}
}
}
# Szczegółowe informacje o konkretnym typie
query GetUserType {
__type(name: "User") {
name
kind
description
fields {
name
type {
name
kind
}
description
args {
name
type {
name
}
}
}
}
}
# Pobranie wszystkich dostępnych queries
query GetAllQueries {
__schema {
queryType {
name
fields {
name
description
type {
name
kind
}
args {
name
type {
name
kind
}
defaultValue
}
}
}
}
}
# Użycie __typename w zwykłym zapytaniu
query GetUserWithTypename {
user(id: "123") {
__typename # Zwróci "User"
id
name
posts {
__typename # Zwróci "Post"
title
}
}
}
# Pełne introspection query (używane przez narzędzia)
query FullIntrospection {
__schema {
queryType { name }
mutationType { name }
subscriptionType { name }
types {
...FullType
}
directives {
name
description
locations
args {
...InputValue
}
}
}
}
fragment FullType on __Type {
kind
name
description
fields(includeDeprecated: true) {
name
description
args {
...InputValue
}
type {
...TypeRef
}
isDeprecated
deprecationReason
}
inputFields {
...InputValue
}
interfaces {
...TypeRef
}
enumValues(includeDeprecated: true) {
name
description
isDeprecated
deprecationReason
}
possibleTypes {
...TypeRef
}
}
fragment InputValue on __InputValue {
name
description
type { ...TypeRef }
defaultValue
}
fragment TypeRef on __Type {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
// Przykład wykorzystania introspection w TypeScript
// Generowanie typów na podstawie schematu
import { getIntrospectionQuery, buildClientSchema } from 'graphql';
async function generateTypes() {
// 1. Pobranie schematu przez introspection
const introspectionQuery = getIntrospectionQuery();
const response = await fetch('http://api.example.com/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: introspectionQuery })
});
const { data } = await response.json();
// 2. Budowanie schematu z introspection data
const schema = buildClientSchema(data);
// 3. Schemat może być użyty do generowania typów TypeScript
// (np. przez graphql-code-generator)
console.log(schema.getQueryType());
}
sequenceDiagram
participant Dev as Developer
participant Tool as GraphiQL/IDE
participant API as GraphQL API
Dev->>Tool: Otwiera GraphiQL
Tool->>API: Wysyła introspection query
API-->>Tool: Zwraca pełny schemat
Tool->>Tool: Buduje dokumentację i autocomplete
Tool-->>Dev: Pokazuje dostępne typy i pola
Dev->>Tool: Pisze zapytanie
Tool->>Tool: Waliduje względem schematu
Tool-->>Dev: Podświetla błędy/sugestie
Materiały
↑ Powrót na góręCzym są fragmenty w GraphQL i kiedy ich używać?
Odpowiedź w 30 sekund:
Fragmenty to reużywalne jednostki pól w GraphQL, które pozwalają uniknąć duplikacji kodu w zapytaniach. Definiują się je słowem kluczowym fragment i używają przez spread operator .... Są szczególnie przydatne przy powtarzających się strukturach danych i dla type safety w aplikacjach z TypeScript.
Odpowiedź w 2 minuty: Fragmenty w GraphQL to mechanizm pozwalający wydzielić wspólne pola do reużywalnych bloków, podobnie jak funkcje w programowaniu. Zamiast powtarzać te same pola w wielu miejscach zapytania, definiujemy je raz jako fragment i używamy wielokrotnie.
Rodzaje fragmentów:
- Named Fragments - nazwane fragmenty, które można reużywać w wielu zapytaniach
- Inline Fragments - używane wewnątrz zapytań dla typów implementujących interface lub będących częścią union
Kiedy używać fragmentów:
- Gdy te same pola są zapytywane w wielu miejscach
- W komponentach React/Vue - każdy komponent definiuje fragment ze swoimi danymi (colocation pattern)
- Przy zapytaniach o typy polimorficzne (interfaces, unions)
- Dla lepszej organizacji kodu i DRY principle
- Przy generowaniu typów TypeScript - każdy fragment generuje osobny typ
Zalety:
- DRY - eliminacja duplikacji kodu
- Modularność - fragmenty można współdzielić między plikami
- Type safety - w TypeScript każdy fragment to osobny typ
- Colocation - komponenty definiują swoje wymagania danych
- Łatwiejsze refaktoryzacje - zmiana w jednym miejscu
Przykład kodu:
# Named Fragment - definicja
fragment UserBasicInfo on User {
id
name
email
avatar
}
fragment UserDetailedInfo on User {
...UserBasicInfo # Fragmenty mogą używać innych fragmentów
age
bio
location
createdAt
}
# Użycie fragmentów w zapytaniu
query GetUsers {
currentUser {
...UserDetailedInfo
}
recentUsers {
...UserBasicInfo
}
user(id: "123") {
...UserDetailedInfo
posts {
id
title
author {
...UserBasicInfo # Reużycie tego samego fragmentu
}
}
}
}
# Inline Fragments - dla typów polimorficznych
# Przykład z Union type
union SearchResult = User | Post | Comment
query Search($term: String!) {
search(term: $term) {
... on User {
id
name
email
}
... on Post {
id
title
content
author {
name
}
}
... on Comment {
id
text
author {
name
}
}
}
}
# Przykład z Interface
interface Node {
id: ID!
createdAt: DateTime!
}
type User implements Node {
id: ID!
createdAt: DateTime!
name: String!
}
type Post implements Node {
id: ID!
createdAt: DateTime!
title: String!
}
query GetNodes {
nodes {
id
createdAt
# Inline fragment dla specyficznych pól
... on User {
name
email
}
... on Post {
title
content
}
}
}
// Przykład użycia fragmentów w React z Apollo Client
import { gql, useQuery } from '@apollo/client';
// Fragment współdzielony przez komponenty
const USER_BASIC_INFO_FRAGMENT = gql`
fragment UserBasicInfo on User {
id
name
email
avatar
}
`;
// Komponent UserCard definiuje swoje wymagania
const UserCard = ({ userId }: { userId: string }) => {
const GET_USER = gql`
${USER_BASIC_INFO_FRAGMENT}
query GetUser($id: ID!) {
user(id: $id) {
...UserBasicInfo
}
}
`;
const { data } = useQuery(GET_USER, { variables: { id: userId } });
return (
<div>
<img src={data?.user.avatar} alt={data?.user.name} />
<h3>{data?.user.name}</h3>
<p>{data?.user.email}</p>
</div>
);
};
// Komponent UserList używa tego samego fragmentu
const UserList = () => {
const GET_USERS = gql`
${USER_BASIC_INFO_FRAGMENT}
query GetUsers {
users {
...UserBasicInfo
}
}
`;
const { data } = useQuery(GET_USERS);
return (
<div>
{data?.users.map(user => (
<UserCard key={user.id} userId={user.id} />
))}
</div>
);
};
# Zaawansowany przykład - fragmenty warunkowe z dyrektywami
fragment UserInfo on User {
id
name
email @include(if: $includeEmail)
posts @skip(if: $skipPosts) {
id
title
}
}
query GetUser($id: ID!, $includeEmail: Boolean!, $skipPosts: Boolean!) {
user(id: $id) {
...UserInfo
}
}
# Fragmenty z różnymi poziomami szczegółowości
fragment UserMinimal on User {
id
name
}
fragment UserStandard on User {
...UserMinimal
email
avatar
}
fragment UserFull on User {
...UserStandard
age
bio
location
createdAt
posts {
id
title
}
followers {
...UserMinimal
}
}
graph TD
A[Query GetUsers] --> B[Fragment UserDetailedInfo]
A --> C[Fragment UserBasicInfo]
B --> C
D[Component UserCard] --> C
E[Component UserList] --> C
F[Component UserProfile] --> B
style C fill:#4fb3d9
style B fill:#e535ab
classDef fragment fill:#f9a825
Materiały
↑ Powrót na góręGraphQL - System Typów
Jak obsługiwać wartości nullable i non-nullable w schemacie?
Odpowiedź w 30 sekund:
W GraphQL domyślnie wszystkie typy są nullable (mogą zwrócić null). Wykrzyknik ! po typie oznacza wartość non-nullable (wymaganą) - String! musi zwrócić string, nie może być null. Dla list: [String] to nullable lista nullable stringów, [String]! to wymagana lista nullable stringów, [String!] to nullable lista wymaganych stringów, a [String!]! to wymagana lista wymaganych stringów (najczęstsza kombinacja dla list).
Odpowiedź w 2 minuty:
System null-safety w GraphQL opiera się na prostej konwencji: typ bez modyfikatora może zwrócić null, natomiast typ z wykrzyknikiem ! musi zawsze zwrócić wartość. Jest to odwrotność wielu języków programowania, gdzie non-nullable jest domyślne. W GraphQL projektanci świadomie uczynili nullable wartością domyślną, uznając że w API dane często mogą być nieobecne (użytkownik bez avatara, opcjonalny opis produktu, itp.).
Dla typów skalarnych i obiektowych reguły są proste: String może być null lub stringiem, String! musi być stringiem. User może być null lub obiektem User, User! musi być obiektem. Jednak listy wprowadzają dodatkową złożoność, ponieważ zarówno sama lista, jak i jej elementy mogą być nullable lub non-nullable niezależnie. [String] - lista może być null, elementy mogą być null. [String]! - lista nigdy nie jest null (może być pusta), ale elementy mogą być null. [String!] - lista może być null, ale jeśli istnieje, wszystkie elementy muszą być stringami. [String!]! - lista nigdy nie jest null i wszystkie elementy muszą być stringami (najpopularniejsza kombinacja dla list).
Walidacja null odbywa się na poziomie runtime podczas wykonywania zapytania. Jeśli pole non-nullable zwróci null, GraphQL propaguje błąd w górę do najbliższego nullable pola lub do data: null na poziomie root, zwracając szczegóły w sekcji errors. To zachowanie chroni spójność danych - klient otrzymuje albo kompletne dane zgodne ze schematem, albo wyraźny sygnał błędu.
Przy projektowaniu schematu należy rozważyć trade-offy: zbyt wiele pól non-nullable czyni API kruche (każdy błąd nulluje całe zapytanie), ale zbyt wiele nullable wymusza obsługę null w każdym kliencie. Dobra praktyka: pola identyfikujące (id) i kluczowe dane domeny powinny być non-nullable, natomiast pola opcjonalne, relacje które mogą być usunięte, i pola podlegające błędom zewnętrznych serwisów powinny być nullable.
Przykład kodu:
# ===== PODSTAWOWE TYPY NULLABLE/NON-NULLABLE =====
type User {
# Non-nullable - zawsze zwraca wartość
id: ID!
username: String!
# Nullable - może zwrócić null
email: String # Użytkownik może nie mieć emaila
avatar: String # Avatar jest opcjonalny
bio: String # Bio może nie być wypełnione
age: Int # Wiek jest opcjonalny
# Non-nullable z wartością domyślną to dobra praktyka
isActive: Boolean!
}
# ===== LISTY - KOMBINACJE NULLABLE =====
type Post {
id: ID!
title: String!
# [String] - lista może być null, elementy mogą być null
# Możliwe wartości: null, [], ["tag1", null, "tag2"]
tags: [String]
# [String]! - lista nigdy nie jest null (może być pusta), elementy mogą być null
# Możliwe wartości: [], ["tag1"], ["tag1", null, "tag2"]
# NIEMOŻLIWE: null
categories: [String]!
# [String!] - lista może być null, ale elementy NIE mogą być null
# Możliwe wartości: null, [], ["tag1", "tag2"]
# NIEMOŻLIWE: ["tag1", null]
keywords: [String!]
# [String!]! - lista nigdy nie jest null, elementy NIE mogą być null
# Możliwe wartości: [], ["tag1", "tag2"]
# NIEMOŻLIWE: null, ["tag1", null]
# To jest NAJCZĘSTSZA kombinacja dla list
relatedTopics: [String!]!
# Listy obiektów - te same zasady
comments: [Comment!]! # Wymagana lista wymaganych komentarzy
likes: [Like] # Opcjonalna lista opcjonalnych like'ów
}
# ===== RELACJE I NULLABLE =====
type Comment {
id: ID!
text: String!
# Autor może być null jeśli konto zostało usunięte
author: User
# Post musi istnieć - komentarz bez posta nie ma sensu
post: Post!
# Odpowiedzi opcjonalne - lista może być pusta ale nie null
replies: [Comment!]!
}
# ===== ARGUMENTS I NULLABLE =====
type Query {
# Argument non-nullable - musi być podany
user(id: ID!): User
# Argumenty nullable - opcjonalne filtry
users(
status: UserStatus
role: UserRole
limit: Int
): [User!]!
# Mix - ID wymagane, reszta opcjonalna
posts(
authorId: ID!
tag: String
published: Boolean
): [Post!]!
}
# ===== INPUT TYPES I NULLABLE =====
input CreateUserInput {
# Pola wymagane przy tworzeniu
username: String!
email: String!
# Pola opcjonalne
bio: String
avatar: String
}
input UpdateUserInput {
# Wszystkie pola opcjonalne - partial update
username: String
email: String
bio: String
avatar: String
}
# ===== PROPAGACJA BŁĘDÓW NULL =====
type Article {
id: ID!
title: String!
# Jeśli author jest non-nullable i zwróci null,
# cały obiekt Article będzie null
author: User!
# Jeśli coAuthor jest nullable i zwróci null,
# tylko to pole będzie null, reszta Article zostanie
coAuthor: User
}
# Przykład odpowiedzi z błędem propagacji null:
# {
# "data": {
# "article": null // Całość null bo author! zwrócił null
# },
# "errors": [{
# "message": "Cannot return null for non-nullable field Article.author",
# "path": ["article", "author"]
# }]
# }
# ===== DOBRE PRAKTYKI =====
type Product {
# Identyfikatory zawsze non-nullable
id: ID!
sku: String!
# Główne dane domeny non-nullable
name: String!
price: Float!
# Opcjonalne dane dodatkowe nullable
description: String
image: String
# Listy zwykle non-nullable (pusta lista zamiast null)
images: [String!]!
variants: [ProductVariant!]!
# Relacje do usuwalnych encji nullable
category: Category # Kategoria może być usunięta
# Wartości boolean z domyślną wartością non-nullable
inStock: Boolean!
featured: Boolean!
}
# ===== ZAGNIEŻDŻONE NON-NULLABLE =====
type Order {
id: ID!
# Lista wymagana, elementy wymagane, pola w elementach wymagane
items: [OrderItem!]!
}
type OrderItem {
id: ID!
quantity: Int!
# Product musi istnieć - OrderItem bez Product to błąd danych
product: Product!
}
# Przykład zapytania pokazującego różne kombinacje:
# query {
# user(id: "123") {
# id # String! - zawsze zwróci wartość
# username # String! - zawsze zwróci wartość
# email # String - może być null
# posts { # [Post!]! - zawsze lista, nigdy pusta pozycja
# title # String! - zawsze
# tags # [String] - może być null lub zawierać nulle
# categories # [String]! - zawsze lista, może zawierać nulle
# keywords # [String!] - może być null, elementy nie
# relatedTopics # [String!]! - zawsze lista, elementy zawsze
# }
# }
# }
Materiały
↑ Powrót na góręJakie typy skalarne są wbudowane w GraphQL?
Odpowiedź w 30 sekund:
GraphQL posiada pięć wbudowanych typów skalarnych: Int (liczby całkowite 32-bitowe), Float (liczby zmiennoprzecinkowe), String (ciągi znaków UTF-8), Boolean (wartości logiczne true/false) oraz ID (unikalny identyfikator serializowany jako string). Typy skalarne reprezentują liście drzewa zapytań - są najprostszymi jednostkami danych, które nie mogą być dalej rozwijane.
Odpowiedź w 2 minuty:
GraphQL definiuje pięć podstawowych typów skalarnych, które stanowią fundament systemu typów. Int reprezentuje liczby całkowite ze znakiem w zakresie 32-bitowym, idealne dla identyfikatorów liczbowych i liczników. Float obsługuje liczby zmiennoprzecinkowe podwójnej precyzji, używane do wartości dziesiętnych jak ceny czy współrzędne. String to sekwencje znaków UTF-8, wykorzystywane do tekstów, dat w formacie ISO czy JSON. Boolean przechowuje wartości logiczne true lub false, często używane w filtrach i flagach.
Typ ID jest specjalnym typem skalarnym zaprojektowanym dla unikalnych identyfikatorów. Chociaż jest serializowany jako string, sygnalizuje klientom i narzędziom, że wartość ta służy jako klucz i nie powinna być czytelna dla człowieka. GraphQL nie wymusza unikalności ID - to odpowiedzialność implementacji serwera.
Oprócz wbudowanych typów, można definiować własne typy skalarne (custom scalars) jak DateTime, URL, Email czy JSON. Wymagają one implementacji funkcji serializacji, deserializacji i walidacji po stronie serwera. Dzięki temu można enkapsulować logikę walidacji i zapewnić bezpieczeństwo typów dla specyficznych formatów danych.
Przykład kodu:
# Użycie wbudowanych typów skalarnych
type User {
id: ID! # Unikalny identyfikator (wymagany)
age: Int # Liczba całkowita (opcjonalna)
rating: Float # Liczba zmiennoprzecinkowa
name: String! # Ciąg znaków (wymagany)
isActive: Boolean # Wartość logiczna
}
# Definiowanie własnego typu skalarnego
scalar DateTime
scalar Email
scalar URL
type Post {
id: ID!
title: String!
createdAt: DateTime! # Własny skalar dla daty
authorEmail: Email! # Własny skalar z walidacją email
thumbnail: URL # Własny skalar dla adresów URL
}
# Zapytanie wykorzystujące typy skalarne
query {
user(id: "123") { # ID jako string
name # String
age # Int
rating # Float
isActive # Boolean
}
}
Materiały
↑ Powrót na góręJak definiować własne typy obiektowe w schemacie GraphQL?
Odpowiedź w 30 sekund:
Typy obiektowe definiuje się używając słowa kluczowego type, po którym następuje nazwa typu i lista pól w nawiasach klamrowych. Każde pole ma nazwę i typ, opcjonalnie z argumentami. Znaki ! oznaczają pola wymagane (non-nullable), a nawiasy kwadratowe [] definiują listy. Typy obiektowe są podstawowymi blokami budulcowymi schematu GraphQL.
Odpowiedź w 2 minuty:
Typy obiektowe w GraphQL definiuje się za pomocą słowa kluczowego type, tworząc nazwany kontener dla pól. Każde pole składa się z nazwy, typu zwracanej wartości oraz opcjonalnych argumentów. Typ może być skalarny, inny typ obiektowy, enum, union lub interface. Modyfikatory typu określają jego kardinalność: brak modyfikatora oznacza wartość opcjonalną nullable, ! wymusza wartość non-nullable, a [Type] definiuje listę.
Pola w typie obiektowym mogą przyjmować argumenty, co umożliwia parametryzację zapytań bezpośrednio na poziomie pola. Argumenty działają podobnie jak parametry funkcji - mają nazwę, typ i opcjonalną wartość domyślną. Jest to potężna funkcjonalność pozwalająca na filtrowanie, paginację czy sortowanie danych bez potrzeby tworzenia dodatkowych zapytań.
GraphQL wspiera dokumentację bezpośrednio w schemacie poprzez komentarze docstring (potrójne cudzysłowy """). Dokumentacja ta jest automatycznie dostępna w narzędziach jak GraphiQL czy GraphQL Playground, co znacznie ułatwia eksplorację API. Dobrą praktyką jest opisywanie każdego typu i pola, szczególnie tych z nieoczywistą semantyką.
Przykład kodu:
# Dokumentacja typu - pojawi się w narzędziach introspection
"""
Reprezentuje użytkownika systemu z pełnym profilem i relacjami.
"""
type User {
# Unikalny identyfikator użytkownika (wymagany)
id: ID!
# Nazwa użytkownika (wymagana)
name: String!
# Adres email (opcjonalny)
email: String
# Wiek użytkownika (opcjonalny, liczba całkowita)
age: Int
# Lista postów użytkownika (wymagana lista, może być pusta)
# Parametr limit ogranicza liczbę zwracanych postów
posts(limit: Int = 10): [Post!]!
# Relacja many-to-many - lista przyjaciół
friends: [User!]!
# Pole z wieloma argumentami dla filtrowania
orders(
status: OrderStatus
minAmount: Float
sortBy: OrderSortField = CREATED_AT
): [Order!]!
}
"""
Post na blogu powiązany z autorem
"""
type Post {
id: ID!
title: String!
content: String!
# Relacja do typu User - pole może być null jeśli autor usunięty
author: User
# Lista tagów (wymagana lista, ale może być pusta)
tags: [String!]!
# Data publikacji (własny skalar)
publishedAt: DateTime
}
# Typ dla zamówień z zagnieżdżonymi obiektami
type Order {
id: ID!
total: Float!
status: OrderStatus!
items: [OrderItem!]!
customer: User!
}
type OrderItem {
id: ID!
quantity: Int!
price: Float!
product: Product!
}
type Product {
id: ID!
name: String!
price: Float!
inStock: Boolean!
}
Materiały
↑ Powrót na góręCzym są typy Input i jak różnią się od zwykłych typów obiektowych?
Odpowiedź w 30 sekund:
Typy Input (definiowane słowem kluczowym input) są specjalnymi typami obiektowymi używanymi wyłącznie jako argumenty w mutations i queries. W przeciwieństwie do zwykłych typów obiektowych, typy Input nie mogą mieć argumentów w polach, nie mogą implementować interfejsów ani zawierać referencji do zwykłych typów obiektowych - tylko do skalarów, enumów i innych typów Input. Służą do strukturyzowania złożonych danych wejściowych.
Odpowiedź w 2 minuty:
Typy Input w GraphQL rozwiązują problem przekazywania złożonych struktur danych jako argumentów do zapytań i mutacji. Definiowane są słowem kluczowym input zamiast type i mają ściśle ograniczoną funkcjonalność w porównaniu do zwykłych typów obiektowych. Ta separacja jest celowa i wynika z fundamentalnych różnic między danymi wejściowymi a wyjściowymi w GraphQL.
Kluczowe ograniczenia typów Input: pola nie mogą przyjmować argumentów (podczas gdy zwykłe typy mogą mieć parametryzowane pola), nie mogą implementować interfejsów ani być częścią unii, oraz mogą zawierać tylko skalary, enumy, listy i inne typy Input. Nie mogą zawierać referencji do zwykłych typów obiektowych. To oznacza, że graf danych wejściowych jest zawsze acykliczny i można go w pełni serializować.
Typy Input są niezbędne przy tworzeniu mutations z wieloma parametrami. Zamiast przekazywać dziesiątki pojedynczych argumentów, można zgrupować je w logiczne struktury Input. Znacznie poprawia to czytelność schematu, ułatwia walidację i umożliwia ponowne wykorzystanie tych samych struktur w różnych mutacjach. Konwencja nazewnictwa sugeruje dodawanie suffiksu Input do nazw tych typów (np. CreateUserInput, UpdatePostInput).
Ponieważ typy Input są używane do modyfikacji danych, często zawierają podzbiór pól z odpowiadających typów obiektowych. Na przykład UserInput może nie zawierać pola id (generowanego przez serwer) ani createdAt (automatycznego timestampu), ale zawiera wszystkie pola edytowalne przez użytkownika.
Przykład kodu:
# Zwykły typ obiektowy dla danych wyjściowych
type User {
id: ID!
username: String!
email: String!
age: Int
posts: [Post!]! # Może zawierać relacje do innych typów
createdAt: DateTime!
updatedAt: DateTime!
}
# Typ Input dla tworzenia użytkownika
input CreateUserInput {
username: String! # Tylko dane edytowalne przez klienta
email: String!
age: Int
# Brak: id, posts, createdAt, updatedAt - generowane przez serwer
}
# Typ Input dla aktualizacji (wszystkie pola opcjonalne)
input UpdateUserInput {
username: String
email: String
age: Int
}
# Zagnieżdżony typ Input dla złożonych struktur
input CreatePostInput {
title: String!
content: String!
tags: [String!]!
# Zagnieżdżony Input object
metadata: PostMetadataInput
}
input PostMetadataInput {
category: String
featured: Boolean
readTime: Int
}
# Mutation używająca typów Input
type Mutation {
# Zamiast wielu pojedynczych argumentów
createUser(input: CreateUserInput!): User!
# ID poza Input object - częsta praktyka
updateUser(id: ID!, input: UpdateUserInput!): User!
# Złożona mutacja z zagnieżdżonymi Inputs
createPost(input: CreatePostInput!): Post!
# Lista Input objects
createMultipleUsers(users: [CreateUserInput!]!): [User!]!
}
# Przykład użycia w mutation
# mutation {
# createUser(input: {
# username: "jan_kowalski"
# email: "jan@example.com"
# age: 25
# }) {
# id
# username
# email
# }
# }
# BŁĘDNE - Input NIE MOŻE zawierać referencji do zwykłych typów
# input InvalidInput {
# name: String!
# author: User! # ❌ BŁĄD - nie można używać typu obiektowego w Input
# }
# POPRAWNE - użyj ID dla referencji
input CreateCommentInput {
text: String!
postId: ID! # ✅ Referencja przez ID
authorId: ID! # ✅ Referencja przez ID
}
Materiały
↑ Powrót na góręResolvers
21. Jak obsługiwać błędy w resolverach GraphQL?
Odpowiedź w 30 sekund:
Błędy w GraphQL można obsługiwać przez rzucanie wyjątków w resolverach, które są automatycznie przechwytywane i zwracane w polu errors odpowiedzi. Można tworzyć niestandardowe klasy błędów, dodawać kody i rozszerzenia, oraz używać formatowania błędów do kontrolowania informacji zwracanych klientowi.
Odpowiedź w 2 minuty:
Obsługa błędów w GraphQL różni się od tradycyjnych REST API, ponieważ GraphQL zawsze zwraca status HTTP 200, a błędy są zawarte w odpowiedzi JSON w polu errors. Podstawowy sposób obsługi błędów to rzucanie wyjątków w resolverach - GraphQL automatycznie je przechwytuje, formatuje i dodaje do odpowiedzi.
GraphQL rozróżnia błędy walidacji (query jest niepoprawny), błędy wykonania (problemy w resolverach) i błędy aplikacyjne (logika biznesowa). Możesz tworzyć niestandardowe klasy błędów dziedziczące po GraphQLError lub ApolloError, które pozwalają na dodawanie kodów błędów, dodatkowych informacji i kategoryzację problemów.
Ważnym aspektem jest kontrolowanie jakie informacje o błędach są wysyłane do klienta. W środowisku produkcyjnym nie chcesz ujawniać szczegółów wewnętrznych błędów (jak stack trace czy błędy bazy danych). Użyj funkcji formatError w konfiguracji serwera do sanityzacji błędów i logowania ich po stronie serwera.
GraphQL pozwala na częściowe sukcesy - jeśli niektóre resolvery nie powiodą się, pola które się udały nadal zostaną zwrócone w polu data, a błędy pojawią się w polu errors. To pozwala klientowi na bardziej gracefulne zarządzanie błędami. Można też wykorzystać union types do modelowania błędów jako część schematu, co daje pełną kontrolę nad obsługą błędów po stronie klienta.
Przykład kodu:
const { ApolloServer, UserInputError, AuthenticationError,
ForbiddenError, ApolloError } = require('apollo-server');
const { GraphQLError } = require('graphql');
// 1. NIESTANDARDOWE KLASY BŁĘDÓW
class NotFoundError extends ApolloError {
constructor(message, properties) {
super(message, 'NOT_FOUND', properties);
Object.defineProperty(this, 'name', { value: 'NotFoundError' });
}
}
class ValidationError extends ApolloError {
constructor(message, validationErrors) {
super(message, 'VALIDATION_ERROR', { validationErrors });
Object.defineProperty(this, 'name', { value: 'ValidationError' });
}
}
class RateLimitError extends ApolloError {
constructor(message, retryAfter) {
super(message, 'RATE_LIMIT_EXCEEDED', { retryAfter });
Object.defineProperty(this, 'name', { value: 'RateLimitError' });
}
}
// 2. RESOLVERY Z RÓŻNYMI TYPAMI BŁĘDÓW
const resolvers = {
Query: {
// Błąd walidacji danych wejściowych
user: async (parent, args, context) => {
const { id } = args;
// Walidacja argumentów
if (!id || id.length < 1) {
throw new UserInputError('ID użytkownika jest wymagane', {
argumentName: 'id',
invalidValue: id
});
}
// Sprawdzenie autoryzacji
if (!context.currentUser) {
throw new AuthenticationError('Musisz być zalogowany aby zobaczyć użytkownika');
}
// Pobieranie danych
const user = await context.db.users.findById(id);
// Błąd nie znaleziono
if (!user) {
throw new NotFoundError(`Użytkownik o ID ${id} nie został znaleziony`, {
userId: id
});
}
// Sprawdzenie uprawnień
if (user.isPrivate && user.id !== context.currentUser.id) {
throw new ForbiddenError('Nie masz uprawnień do przeglądania tego profilu');
}
return user;
},
// Obsługa wielu błędów walidacji
createUser: async (parent, args, context) => {
const { input } = args;
const errors = [];
// Zbieranie błędów walidacji
if (!input.email || !input.email.includes('@')) {
errors.push({
field: 'email',
message: 'Nieprawidłowy adres email'
});
}
if (!input.password || input.password.length < 8) {
errors.push({
field: 'password',
message: 'Hasło musi mieć minimum 8 znaków'
});
}
if (input.age && input.age < 18) {
errors.push({
field: 'age',
message: 'Musisz mieć minimum 18 lat'
});
}
// Jeśli są błędy, rzuć wyjątek z wszystkimi
if (errors.length > 0) {
throw new ValidationError('Dane wejściowe są nieprawidłowe', errors);
}
// Sprawdzenie unikalności email
const existingUser = await context.db.users.findByEmail(input.email);
if (existingUser) {
throw new UserInputError('Email jest już zajęty', {
field: 'email',
value: input.email
});
}
try {
return await context.db.users.create(input);
} catch (dbError) {
// Obsługa błędów bazy danych
context.logger.error('Database error:', dbError);
throw new ApolloError(
'Wystąpił błąd podczas tworzenia użytkownika',
'DATABASE_ERROR'
);
}
},
// Rate limiting
searchUsers: async (parent, args, context) => {
const userId = context.currentUser?.id;
// Sprawdzenie rate limit
const isRateLimited = await context.rateLimiter.check(userId);
if (isRateLimited) {
const retryAfter = await context.rateLimiter.getRetryAfter(userId);
throw new RateLimitError(
'Zbyt wiele zapytań. Spróbuj ponownie później.',
retryAfter
);
}
return await context.db.users.search(args.query);
}
},
User: {
// Graceful error handling - zwróć null zamiast rzucać błąd
email: async (parent, args, context) => {
// Email tylko dla właściciela lub admina
if (context.currentUser?.id !== parent.id &&
!context.currentUser?.isAdmin) {
// Zamiast rzucać błąd, zwróć null
return null;
}
return parent.email;
},
// Obsługa błędów z try-catch
statistics: async (parent, args, context) => {
try {
return await context.analyticsService.getUserStats(parent.id);
} catch (error) {
// Logowanie błędu
context.logger.error('Error fetching user statistics', {
userId: parent.id,
error: error.message
});
// Zwróć domyślne wartości zamiast rzucać błąd
return {
postsCount: 0,
followersCount: 0,
followingCount: 0
};
}
}
}
};
// 3. FORMATOWANIE I LOGOWANIE BŁĘDÓW
const server = new ApolloServer({
typeDefs,
resolvers,
// Formatowanie błędów przed wysłaniem do klienta
formatError: (error) => {
// Logowanie wszystkich błędów
console.error('GraphQL Error:', {
message: error.message,
code: error.extensions?.code,
path: error.path,
locations: error.locations
});
// Nie ujawniaj szczegółów błędów wewnętrznych w produkcji
if (process.env.NODE_ENV === 'production') {
// Ukryj stack trace
delete error.extensions?.exception?.stacktrace;
// Zamień błędy bazy danych na ogólny komunikat
if (error.extensions?.code === 'DATABASE_ERROR') {
return new GraphQLError(
'Wystąpił błąd serwera. Spróbuj ponownie później.',
{
extensions: {
code: 'INTERNAL_SERVER_ERROR'
}
}
);
}
// Ukryj szczegóły nieprzewidzianych błędów
if (error.extensions?.code === 'INTERNAL_SERVER_ERROR') {
return new GraphQLError(
'Wystąpił nieoczekiwany błąd',
{
extensions: {
code: 'INTERNAL_SERVER_ERROR'
}
}
);
}
}
return error;
},
// Pluginy do zaawansowanego logowania
plugins: [
{
requestDidStart(requestContext) {
return {
didEncounterErrors(ctx) {
// Szczegółowe logowanie błędów
if (!ctx.operation) return;
for (const error of ctx.errors) {
// Loguj do zewnętrznego systemu (Sentry, CloudWatch, etc.)
logger.error({
message: error.message,
code: error.extensions?.code,
path: error.path,
operation: ctx.operation.name?.value,
variables: ctx.request.variables,
userId: ctx.context.currentUser?.id,
timestamp: new Date().toISOString()
});
}
}
};
}
}
]
});
// 4. PRZYKŁAD ODPOWIEDZI Z BŁĘDAMI
/*
Zapytanie:
query {
user(id: "999") {
id
name
email
}
}
Odpowiedź z błędem:
{
"data": {
"user": null
},
"errors": [
{
"message": "Użytkownik o ID 999 nie został znaleziony",
"locations": [{ "line": 2, "column": 3 }],
"path": ["user"],
"extensions": {
"code": "NOT_FOUND",
"userId": "999"
}
}
]
}
Częściowy sukces (niektóre pola się powiodły):
query {
users {
id
name
email # Może zwrócić null dla niektórych
statistics # Może zwrócić domyślne wartości przy błędzie
}
}
Odpowiedź:
{
"data": {
"users": [
{
"id": "1",
"name": "Jan Kowalski",
"email": "jan@example.com",
"statistics": { "postsCount": 10 }
},
{
"id": "2",
"name": "Anna Nowak",
"email": null, # Brak uprawnień - zwrócono null
"statistics": { "postsCount": 0 } # Błąd serwisu - domyślne wartości
}
]
},
"errors": [] # Błędy obsłużone gracefully
}
*/
// 5. ALTERNATYWNE PODEJŚCIE: BŁĘDY JAKO CZĘŚĆ SCHEMATU
const typeDefs = `
type Query {
user(id: ID!): UserResult!
}
union UserResult = User | NotFoundError | ForbiddenError
type User {
id: ID!
name: String!
}
type NotFoundError {
message: String!
userId: ID!
}
type ForbiddenError {
message: String!
reason: String!
}
`;
const resolvers = {
Query: {
user: async (parent, args, context) => {
const user = await context.db.users.findById(args.id);
if (!user) {
// Zwróć błąd jako część danych
return {
__typename: 'NotFoundError',
message: 'Użytkownik nie znaleziony',
userId: args.id
};
}
if (user.isPrivate && user.id !== context.currentUser?.id) {
return {
__typename: 'ForbiddenError',
message: 'Brak dostępu',
reason: 'Profil jest prywatny'
};
}
return {
__typename: 'User',
...user
};
}
},
UserResult: {
__resolveType(obj) {
return obj.__typename;
}
}
};
// Zapytanie z obsługą błędów po stronie klienta:
/*
query {
user(id: "123") {
... on User {
id
name
}
... on NotFoundError {
message
userId
}
... on ForbiddenError {
message
reason
}
}
}
*/
Materiały:
↑ Powrót na góręGraphQL - Bezpieczeństwo
Jakie są główne zagrożenia bezpieczeństwa w GraphQL API?
Odpowiedź w 30 sekund: Główne zagrożenia to: ataki Denial of Service przez zbyt złożone zapytania (query complexity attacks), nieautoryzowany dostęp do danych przez introspection, wyciek informacji przez szczegółowe komunikaty błędów, brak rate limitingu umożliwiający brute force, oraz narażenie wrażliwych danych przez nadmiernie permisywne resolvery. GraphQL pozwala klientom na precyzyjne określanie zapytań, co może być wykorzystane do obejścia standardowych zabezpieczeń REST API.
Odpowiedź w 2 minuty: GraphQL wprowadza unikalne wyzwania bezpieczeństwa ze względu na swoją elastyczność. Pierwszym zagrożeniem są ataki DoS przez złożoność zapytań - klient może wysłać głęboko zagnieżdżone lub cykliczne zapytania (np. użytkownicy -> posty -> komentarze -> użytkownicy), które mogą przeciążyć serwer i bazę danych. Bez limitów głębokości i złożoności, pojedyncze zapytanie może wywołać tysiące operacji bazodanowych.
Drugim problemem jest nieautoryzowany dostęp do danych. W przeciwieństwie do REST, gdzie każdy endpoint wymaga osobnej autoryzacji, GraphQL często ma jeden endpoint. Jeśli autoryzacja nie jest implementowana na poziomie pól i resolverów, klient może żądać wrażliwych danych, do których nie powinien mieć dostępu. Introspection, mimo że przydatne w rozwoju, może ujawnić całą strukturę API, pomagając atakującym.
Injection attacks są kolejnym zagrożeniem - jeśli dane wejściowe nie są walidowane, możliwe są ataki SQL injection lub NoSQL injection w resolverach. Szczegółowe komunikaty błędów mogą ujawniać wrażliwą strukturę bazy danych lub logikę biznesową. Brak rate limitingu jest szczególnie niebezpieczny, gdyż każde zapytanie GraphQL może wykonywać różną ilość pracy - standard HTTP rate limiting (np. 1000 req/min) nie uwzględnia tego, że jedno zapytanie GraphQL może być równoważne tysiącom REST requestów.
Dodatkowo, batch attacks mogą obejść standardowe zabezpieczenia - atakujący może wysłać wiele mutacji w jednym zapytaniu (np. 1000 prób logowania). Wreszcie, field duplication pozwala na wielokrotne żądanie tego samego pola z różnymi aliasami, potencjalnie obchodząc limity i przeciążając system.
Przykład kodu:
// Przykłady zagrożeń bezpieczeństwa
// 1. Atak przez złożoność zapytania (Query Complexity Attack)
const maliciousQuery = `
query DangerousQuery {
users {
posts {
comments {
author {
posts {
comments {
author {
posts {
# ... i tak dalej - głęboko zagnieżdżone
}
}
}
}
}
}
}
}
}
`;
// 2. Batch Attack - wielokrotne próby logowania w jednym zapytaniu
const batchAttack = `
mutation BruteForce {
attempt1: login(email: "admin@example.com", password: "password1") { token }
attempt2: login(email: "admin@example.com", password: "password2") { token }
attempt3: login(email: "admin@example.com", password: "password3") { token }
# ... 1000 prób w jednym zapytaniu
}
`;
// 3. Field Duplication Attack - obejście limitów
const fieldDuplication = `
query ExpensiveQuery {
field1: expensiveField { data }
field2: expensiveField { data }
field3: expensiveField { data }
# ... setki aliasów dla tego samego pola
}
`;
// 4. Introspection ujawniający strukturę API
const introspectionQuery = `
query IntrospectionQuery {
__schema {
types {
name
fields {
name
args {
name
type { name }
}
}
}
}
}
`;
// 5. Injection Attack - brak walidacji
// Niebezpieczny resolver podatny na injection
const vulnerableResolver = {
Query: {
user: async (_, { id }, { db }) => {
// NIEBEZPIECZNE - SQL Injection!
const query = `SELECT * FROM users WHERE id = ${id}`;
return db.query(query);
}
}
};
// 6. Nieautoryzowany dostęp do wrażliwych danych
const unauthorizedQuery = `
query SensitiveData {
users {
email # Dane publiczne
password # WRAŻLIWE - nie powinno być dostępne!
creditCard # WRAŻLIWE - nie powinno być dostępne!
ssn # WRAŻLIWE - nie powinno być dostępne!
}
}
`;
Materiały
↑ Powrót na góręGraphQL - Queries i Mutations
Jak konstruować zapytania z zagnieżdżonymi polami w GraphQL?
Odpowiedź w 30 sekund: Zagnieżdżone pola w GraphQL pozwalają na pobieranie powiązanych danych w jednym zapytaniu. Struktura zapytania odzwierciedla kształt zwracanych danych - każde pole może zawierać własne pod-pola, tworząc hierarchię. GraphQL automatycznie rozwiązuje relacje między obiektami, eliminując problem N+1 queries przy odpowiedniej implementacji resolverów.
Odpowiedź w 2 minuty: Zapytania z zagnieżdżonymi polami są jedną z najważniejszych cech GraphQL, pozwalającą na pobieranie powiązanych danych w pojedynczym żądaniu. Struktura zapytania jest hierarchiczna - każdy obiekt może zawierać pola będące innymi obiektami, które również mogą być rozwijane. Klient precyzyjnie określa, jakie dane i na jakiej głębokości chce otrzymać.
GraphQL automatycznie wykonuje wszystkie niezbędne operacje bazodanowe lub wywołania serwisów, aby dostarczyć kompletne dane zgodnie z zapytaniem. Serwer używa resolverów dla każdego pola, które są wykonywane hierarchicznie - najpierw resolver głównego pola, potem resolvery jego pod-pól. Pozwala to na optymalizację zapytań poprzez techniki takie jak DataLoader, który grupuje i deduplikuje zapytania.
Głębokość zagnieżdżenia jest nieograniczona teoretycznie, ale w praktyce powinna być kontrolowana przez mechanizmy zabezpieczeń po stronie serwera (query depth limiting, query complexity analysis). Zagnieżdżone zapytania mogą również zawierać argumenty na każdym poziomie, co daje dużą elastyczność w filtrowaniu i parametryzacji danych.
Kluczową zaletą jest możliwość pobrania wszystkich potrzebnych danych w jednym round-trip do serwera, zamiast wykonywania wielu kolejnych zapytań (jak w REST API). Kształt otrzymanych danych dokładnie odpowiada strukturze zapytania, co czyni GraphQL bardzo przewidywalnym i łatwym w użyciu.
Przykład kodu:
# Proste zapytanie z zagnieżdżonymi polami
query {
user(id: "123") {
name
email
# Zagnieżdżone pole - lista postów użytkownika
posts {
id
title
# Kolejny poziom zagnieżdżenia - komentarze do posta
comments {
id
content
author {
name
avatar
}
}
}
# Zagnieżdżone pole - profil użytkownika
profile {
bio
website
# Dane adresowe w profilu
address {
city
country
}
}
}
}
# Przykład z argumentami na różnych poziomach
query {
organization(id: "456") {
name
# Filtrowanie na poziomie zagnieżdżonym
repositories(first: 10, orderBy: {field: CREATED_AT, direction: DESC}) {
name
starCount
# Paginacja na głębszym poziomie
issues(first: 5, state: OPEN) {
title
createdAt
assignees(first: 3) {
login
avatarUrl
}
}
}
}
}
# Kompleksowy przykład z wieloma poziomami
query GetUserDashboard {
currentUser {
id
username
# Statystyki użytkownika
stats {
postCount
followerCount
followingCount
}
# Lista obserwujących z ich danymi
followers(limit: 20) {
id
username
avatar
# Sprawdzamy czy my też ich obserwujemy
isFollowedByMe
}
# Tablica wątków z najnowszymi wiadomościami
conversations(unreadOnly: true) {
id
participants {
username
avatar
lastSeen
}
lastMessage {
content
sentAt
sender {
username
}
# Informacja o załącznikach
attachments {
type
url
filename
}
}
}
}
}
Materiały
↑ Powrót na góręGraphQL - Subscriptions
Jak działają Subscriptions w GraphQL i do czego służą?
Odpowiedź w 30 sekund: Subscriptions to trzeci typ operacji w GraphQL (obok Query i Mutation), który umożliwia przesyłanie danych w czasie rzeczywistym z serwera do klienta. Działa w modelu publish-subscribe, gdzie klient subskrybuje określone zdarzenia, a serwer automatycznie wysyła aktualizacje gdy te zdarzenia występują. Jest to idealne rozwiązanie dla komunikatorów, powiadomień i dashboardów na żywo.
Odpowiedź w 2 minuty: Subscriptions w GraphQL to mechanizm umożliwiający dwukierunkową komunikację w czasie rzeczywistym między serwerem a klientem. W przeciwieństwie do tradycyjnego modelu request-response (stosowanego w Queries i Mutations), Subscriptions wykorzystują długotrwałe połączenie, przez które serwer może proaktywnie wysyłać dane do klienta.
Proces działania jest następujący: klient inicjuje subskrypcję poprzez wysłanie specjalnej operacji GraphQL typu subscription. Serwer nawiązuje wtedy trwałe połączenie (zazwyczaj przez WebSocket) i rejestruje klienta jako odbiorcę określonych zdarzeń. Gdy na serwerze wystąpi zdarzenie (np. nowy komentarz, zmiana statusu zamówienia, nowa wiadomość), serwer automatycznie wysyła aktualizację do wszystkich zasubskrybowanych klientów.
Subscriptions są szczególnie przydatne w aplikacjach wymagających aktualizacji w czasie rzeczywistym: aplikacje czatowe, systemy powiadomień, dashboardy z danymi na żywo, współpraca w czasie rzeczywistym (collaborative editing), gry multiplayer, monitorowanie systemów czy giełdy i wykresy finansowe. Pozwalają uniknąć kosztownego pollingu (ciągłego odpytywania serwera), zachowując jednocześnie spójność API GraphQL.
W schemacie definiujemy Subscriptions podobnie jak Queries czy Mutations, używając typu Subscription. Każde pole w tym typie reprezentuje osobną subskrypcję, do której klienci mogą się podłączyć.
Przykład kodu:
// Definicja schematu GraphQL z Subscriptions
const typeDefs = `
type Message {
id: ID!
content: String!
author: String!
timestamp: String!
}
type Query {
messages: [Message!]!
}
type Mutation {
sendMessage(content: String!, author: String!): Message!
}
type Subscription {
# Subskrypcja nowych wiadomości
messageAdded: Message!
# Subskrypcja z argumentem - tylko wiadomości konkretnego pokoju
messageAddedToRoom(roomId: ID!): Message!
}
`;
// Implementacja resolverów z Subscriptions (Apollo Server)
const { PubSub } = require('graphql-subscriptions');
const pubsub = new PubSub();
const resolvers = {
Query: {
messages: () => {
// Pobierz wszystkie wiadomości
return messageDatabase.getAll();
}
},
Mutation: {
sendMessage: (parent, { content, author }) => {
const message = {
id: Date.now().toString(),
content,
author,
timestamp: new Date().toISOString()
};
// Zapisz wiadomość w bazie
messageDatabase.save(message);
// Opublikuj zdarzenie dla wszystkich subskrybentów
pubsub.publish('MESSAGE_ADDED', {
messageAdded: message
});
return message;
}
},
Subscription: {
messageAdded: {
// Zwraca AsyncIterator, który emituje zdarzenia
subscribe: () => pubsub.asyncIterator(['MESSAGE_ADDED'])
},
messageAddedToRoom: {
subscribe: (parent, { roomId }) => {
// Subskrypcja z filtrowaniem po ID pokoju
return pubsub.asyncIterator([`MESSAGE_ADDED_${roomId}`]);
}
}
}
};
// Użycie po stronie klienta (Apollo Client)
import { useSubscription, gql } from '@apollo/client';
const MESSAGE_SUBSCRIPTION = gql`
subscription OnMessageAdded {
messageAdded {
id
content
author
timestamp
}
}
`;
function ChatComponent() {
const { data, loading, error } = useSubscription(MESSAGE_SUBSCRIPTION);
if (loading) return <p>Łączenie z czatem...</p>;
if (error) return <p>Błąd połączenia: {error.message}</p>;
// Nowa wiadomość automatycznie pojawi się w data.messageAdded
return (
<div>
{data?.messageAdded && (
<div className="new-message">
<strong>{data.messageAdded.author}</strong>: {data.messageAdded.content}
</div>
)}
</div>
);
}
// Diagram przepływu danych w Subscriptions
/*
┌──────────┐ ┌──────────┐
│ Klient │ │ Serwer │
│ A │ │ GraphQL │
└──────────┘ └──────────┘
│ │
│ 1. subscription { messageAdded } │
│─────────────────────────────────────>│
│ │
│ 2. WebSocket Connection Opened │
│<─────────────────────────────────────│
│ │
┌──────────┐ │
│ Klient │ │
│ B │ │
└──────────┘ │
│ │
│ 3. mutation { sendMessage(...) } │
│─────────────────────────────────────>│
│ │
│ 4. Return mutation result │ Publish event
│<─────────────────────────────────────│ MESSAGE_ADDED
│ │
┌──────────┐ │
│ Klient │ │
│ A │ │
└──────────┘ │
│ 5. Push new message data │
│<─────────────────────────────────────│
│ { messageAdded: {...} } │
│ │
*/
Materiały
- GraphQL Subscriptions - oficjalna dokumentacja
- Apollo Server Subscriptions
- How to GraphQL - Subscriptions
GraphQL - Wydajność i Optymalizacja
26. Czym jest problem N+1 w GraphQL i jak go rozwiązać?
Odpowiedź w 30 sekund: Problem N+1 występuje, gdy wykonujemy 1 zapytanie po listę obiektów, a następnie N dodatkowych zapytań po powiązane dane dla każdego obiektu. W GraphQL jest szczególnie powszechny ze względu na zagnieżdżoną naturę zapytań. Rozwiązaniem jest użycie DataLoader lub optymalizacja resolverów z batchingiem.
Odpowiedź w 2 minuty: Problem N+1 to klasyczny problem wydajnościowy, który w GraphQL manifestuje się bardzo łatwo. Wyobraź sobie zapytanie pobierające listę użytkowników wraz z ich postami - jeśli nie zoptymalizujemy resolverów, GraphQL wykona 1 zapytanie po użytkowników, a następnie osobne zapytanie po posty dla każdego użytkownika (N zapytań). Dla 100 użytkowników to 101 zapytań do bazy danych.
Problem jest szczególnie podstępny w GraphQL, ponieważ klient może dowolnie zagnieżdżać zapytania i łatwo o nieświadome wywołanie dziesiątek lub setek zapytań. Serwer musi być odpowiedzialny za optymalizację, niezależnie od struktury zapytania klienta.
Główne rozwiązania to: użycie biblioteki DataLoader (Facebook), która automatycznie grupuje i cachuje zapytania w ramach pojedynczego requesta; optymalizacja zapytań SQL przez użycie JOIN zamiast osobnych SELECT-ów; implementacja własnego mechanizmu batchingu w resolverach; lub użycie technik jak query planning, gdzie analizujemy całe zapytanie GraphQL przed wykonaniem i generujemy optymalny plan zapytań do bazy.
Monitoring i logowanie zapytań do bazy danych jest kluczowe dla wykrywania problemów N+1 w produkcji. Wiele narzędzi developerskich dla GraphQL (jak Apollo Studio) oferuje analizę wydajności resolverów.
Przykład kodu:
// ❌ PROBLEM N+1 - Nieoptymalne
const resolvers = {
Query: {
users: () => db.users.findAll() // 1 zapytanie
},
User: {
// N zapytań - wykonywane osobno dla każdego użytkownika!
posts: (user) => db.posts.findByUserId(user.id)
}
};
// ✅ ROZWIĄZANIE 1: DataLoader
const DataLoader = require('dataloader');
// Funkcja batchująca - pobiera posty dla wielu userów naraz
const batchGetPostsByUserId = async (userIds) => {
const posts = await db.posts.findByUserIds(userIds);
// Grupujemy posty według userId
const postsByUserId = userIds.map(id =>
posts.filter(post => post.userId === id)
);
return postsByUserId;
};
// Tworzymy loader (jeden na request!)
const createLoaders = () => ({
postsByUserId: new DataLoader(batchGetPostsByUserId)
});
const resolvers = {
Query: {
users: () => db.users.findAll()
},
User: {
// DataLoader automatycznie zbiera wszystkie wywołania
// i wykonuje je jako jeden batch
posts: (user, args, { loaders }) =>
loaders.postsByUserId.load(user.id)
}
};
// W konfiguracji serwera
const server = new ApolloServer({
typeDefs,
resolvers,
context: () => ({
loaders: createLoaders() // Nowy loader dla każdego requesta
})
});
// ✅ ROZWIĄZANIE 2: Optymalizacja na poziomie bazy danych
const resolvers = {
Query: {
users: async () => {
// Jedno zapytanie z JOIN
const usersWithPosts = await db.query(`
SELECT
u.id, u.name, u.email,
p.id as post_id, p.title, p.content
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
`);
// Przekształcamy płaski wynik w zagnieżdżoną strukturę
return groupUserPosts(usersWithPosts);
}
}
};
// ✅ ROZWIĄZANIE 3: Własny batching w resolverze
let batchQueue = [];
let batchTimer = null;
const resolvers = {
User: {
posts: (user) => {
return new Promise((resolve) => {
// Dodajemy do kolejki
batchQueue.push({ userId: user.id, resolve });
// Planujemy wykonanie batcha
if (!batchTimer) {
batchTimer = setTimeout(async () => {
const userIds = batchQueue.map(item => item.userId);
const allPosts = await db.posts.findByUserIds(userIds);
// Rozdzielamy wyniki
batchQueue.forEach(({ userId, resolve }) => {
resolve(allPosts.filter(p => p.userId === userId));
});
// Reset kolejki
batchQueue = [];
batchTimer = null;
}, 0);
}
});
}
}
};
Materiały
↑ Powrót na góręGraphQL - Narzędzia i Ekosystem
Jakie są popularne biblioteki do budowania GraphQL API (Apollo, Yoga, etc.)?
Odpowiedź w 30 sekund: Do budowania GraphQL API najpopularniejsze są Apollo Server (najpełniejsze rozwiązanie z caching i integracjami), GraphQL Yoga (lekki i nowoczesny z obsługą subscriptions), Express-GraphQL (minimalistyczny dla Express), Mercurius (ultra-szybki dla Fastify) oraz AWS AppSync (zarządzany serwis w chmurze). Wybór zależy od wymagań projektu, ekosystemu i preferencji architektonicznych.
Odpowiedź w 2 minuty: Apollo Server to najbardziej kompletna i popularna biblioteka oferująca gotowe rozwiązania dla caching, error handling, plugins, integrations z bazami danych i monitorowanie. Jest idealna dla dużych aplikacji enterprise wymagających pełnego ekosystemu narzędzi. Apollo Federation umożliwia budowanie distributed GraphQL architectures.
GraphQL Yoga (by The Guild) to nowoczesna, lekka alternatywa z wbudowaną obsługą subscriptions, file uploads, oraz CORS. Oferuje plugin system i doskonałą developer experience. Jest zbudowana na bazie Envelop - modularnego systemu pluginów GraphQL.
Express-GraphQL to minimalistyczne rozwiązanie integrujące GraphQL z Express.js, idealne dla prostych API lub gdy chcemy mieć pełną kontrolę. Mercurius oferuje najlepszą wydajność dzięki integracji z Fastify i jest 10x szybszy od innych rozwiązań. Dodatkowo istnieją narzędzia jak Hasura (instant GraphQL dla PostgreSQL), Prisma (ORM z GraphQL), PostGraphile (automatyczne API z PostgreSQL schema) oraz AWS AppSync dla serverless architectures.
Wybór biblioteki zależy od kilku czynników: wielkości projektu, wymagań wydajnościowych, potrzeby federacji, preferencji frameworka backend oraz dostępności gotowych integracji z bazami danych i serwisami zewnętrznymi.
Przykład kodu:
// Apollo Server - pełne rozwiązanie enterprise
const { ApolloServer } = require('@apollo/server');
const { startStandaloneServer } = require('@apollo/server/standalone');
const server = new ApolloServer({
typeDefs: `
type Query {
books: [Book]
}
type Book {
title: String
author: String
}
`,
resolvers: {
Query: {
books: () => books
}
},
// Wbudowane features
cache: 'bounded',
plugins: [
// Monitoring, tracing, error reporting
]
});
const { url } = await startStandaloneServer(server, {
port: 4000
});
// GraphQL Yoga - nowoczesne, lekkie rozwiązanie
import { createYoga, createSchema } from 'graphql-yoga';
import { createServer } from 'node:http';
const yoga = createYoga({
schema: createSchema({
typeDefs: `
type Query {
hello: String
}
type Subscription {
countdown(from: Int!): Int!
}
`,
resolvers: {
Query: {
hello: () => 'Hello from Yoga!'
},
Subscription: {
countdown: {
// Wbudowana obsługa subscriptions
subscribe: async function* (_, { from }) {
for (let i = from; i >= 0; i--) {
await new Promise(resolve => setTimeout(resolve, 1000));
yield { countdown: i };
}
}
}
}
}
}),
// Automatyczne CORS, GraphiQL, file uploads
graphiql: true,
cors: true
});
createServer(yoga).listen(4000);
// Mercurius - ultra szybkie rozwiązanie dla Fastify
import Fastify from 'fastify';
import mercurius from 'mercurius';
const app = Fastify();
app.register(mercurius, {
schema: `
type Query {
add(x: Int, y: Int): Int
}
`,
resolvers: {
Query: {
add: async (_, { x, y }) => x + y
}
},
// JIT compilation dla maksymalnej wydajności
jit: 1,
// Wbudowany caching
cache: true,
graphiql: true
});
await app.listen({ port: 4000 });
// Express-GraphQL - minimalistyczne dla Express
import express from 'express';
import { graphqlHTTP } from 'express-graphql';
import { buildSchema } from 'graphql';
const app = express();
app.use('/graphql', graphqlHTTP({
schema: buildSchema(`
type Query {
hello: String
}
`),
rootValue: {
hello: () => 'Hello world!'
},
graphiql: true
}));
app.listen(4000);
// Hasura - instant GraphQL API dla PostgreSQL
// Konfiguracja przez UI lub metadata:
// tables.yaml
table:
name: books
schema: public
object_relationships:
- name: author
using:
foreign_key_constraint_on: author_id
// Automatycznie generuje:
// query { books { id title author { name } } }
// mutation { insert_books(objects: {...}) { affected_rows } }
Materiały
- Apollo Server Documentation
- GraphQL Yoga
- Mercurius - GraphQL adapter for Fastify
- Express-GraphQL
- Hasura GraphQL Engine
- Comparison of GraphQL Server Libraries