GraphQL - Pytania Rekrutacyjne i Kompletny Przewodnik 2026
"Wyjaśnij różnicę między GraphQL a REST" - to pytanie otwiera większość rozmów o nowoczesnych API. GraphQL stał się standardem w wielu firmach, od startupów po gigantów jak Facebook, GitHub, Shopify. Rekruterzy oczekują nie tylko znajomości składni, ale zrozumienia kiedy GraphQL ma sens, jak rozwiązać N+1 problem, i jak zaimplementować autentykację.
W tym przewodniku znajdziesz 50+ pytań rekrutacyjnych z odpowiedziami, od podstaw GraphQL po zaawansowane tematy jak Federation, caching i optymalizacja wydajności.
GraphQL vs REST - Podstawy
Odpowiedź w 30 sekund
"GraphQL to język zapytań do API gdzie klient określa dokładnie jakie dane chce otrzymać. W przeciwieństwie do REST z wieloma endpointami, GraphQL używa jednego endpointu i pozwala pobierać powiązane dane w jednym zapytaniu. Ma silne typowanie przez schema, co eliminuje over-fetching i under-fetching."
Odpowiedź w 2 minuty
REST i GraphQL to dwa różne podejścia do projektowania API. REST opiera się na zasobach i metodach HTTP, GraphQL na typach i zapytaniach. Kluczowa różnica jest w tym, kto kontroluje kształt odpowiedzi.
┌─────────────────────────────────────────────────────────────────┐
│ REST vs GraphQL │
├─────────────────────────────────────────────────────────────────┤
│ │
│ REST: Serwer decyduje co zwrócić │
│ ──────────────────────────────── │
│ GET /users/1 │
│ → { id, name, email, avatar, createdAt, settings, ... } │
│ │
│ GET /users/1/posts │
│ → [{ id, title, content, ... }, ...] │
│ │
│ GET /users/1/posts/5/comments │
│ → [{ id, text, author, ... }, ...] │
│ │
│ = 3 requesty, dużo niepotrzebnych danych (over-fetching) │
│ │
│ ───────────────────────────────────────────────────────────── │
│ │
│ GraphQL: Klient decyduje co chce │
│ ──────────────────────────────── │
│ query { │
│ user(id: 1) { │
│ name │
│ posts(first: 5) { │
│ title │
│ comments(first: 3) { │
│ text │
│ author { name } │
│ } │
│ } │
│ } │
│ } │
│ │
│ = 1 request, dokładnie te dane które potrzebujemy │
└─────────────────────────────────────────────────────────────────┘
Porównanie szczegółowe:
| Aspekt | REST | GraphQL |
|---|---|---|
| Endpointy | Wiele (/users, /posts) | Jeden (/graphql) |
| Data fetching | Fixed response | Client-specified |
| Typowanie | Opcjonalne (OpenAPI) | Wbudowane (Schema) |
| Wersjonowanie | URL (/v1/, /v2/) | Schema evolution |
| Caching | HTTP cache (łatwe) | Wymaga strategii |
| Real-time | Polling/WebHooks | Subscriptions |
| Learning curve | Niższa | Wyższa |
Schema i Typy - Pytania Rekrutacyjne
1. Jak zdefiniować schema w GraphQL?
Odpowiedź:
Schema to kontrakt między klientem a serwerem. Definiuje typy danych, queries, mutations i subscriptions.
# Podstawowe typy skalarne
# String, Int, Float, Boolean, ID
# Custom type
type User {
id: ID! # ! = non-nullable
name: String!
email: String!
age: Int
posts: [Post!]! # Lista non-null postów
role: Role!
createdAt: DateTime! # Custom scalar
}
type Post {
id: ID!
title: String!
content: String
author: User!
comments: [Comment!]!
tags: [String!]
status: PostStatus!
}
type Comment {
id: ID!
text: String!
author: User!
post: Post!
}
# Enum
enum Role {
USER
ADMIN
MODERATOR
}
enum PostStatus {
DRAFT
PUBLISHED
ARCHIVED
}
# Input type (dla mutations)
input CreatePostInput {
title: String!
content: String
tags: [String!]
}
input UpdatePostInput {
title: String
content: String
tags: [String!]
}
# Query - read operations
type Query {
user(id: ID!): User
users(limit: Int, offset: Int): [User!]!
post(id: ID!): Post
posts(status: PostStatus, first: Int, after: String): PostConnection!
me: User # Current authenticated user
}
# Mutation - write operations
type Mutation {
createUser(name: String!, email: String!): User!
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: UpdatePostInput!): Post
deletePost(id: ID!): Boolean!
login(email: String!, password: String!): AuthPayload!
}
# Subscription - real-time
type Subscription {
postCreated: Post!
commentAdded(postId: ID!): Comment!
}
# Custom types for auth
type AuthPayload {
token: String!
user: User!
}
# Relay-style pagination
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type PostEdge {
node: Post!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
Nullable vs Non-nullable:
field: String # Może być null
field: String! # Nigdy null
field: [String] # Lista lub null, elementy mogą być null
field: [String!] # Lista lub null, elementy non-null
field: [String!]!# Lista non-null, elementy non-null
2. Co to są resolvers i jak działają?
Słaba odpowiedź: "Resolvers zwracają dane dla pól."
Mocna odpowiedź:
Resolver to funkcja odpowiedzialna za zwrócenie danych dla konkretnego pola w schema. GraphQL wykonuje resolvers rekurencyjnie, budując odpowiedź.
// Resolver signature
// (parent, args, context, info) => result
const resolvers = {
Query: {
// Root resolver - parent jest undefined
user: async (_, { id }, context) => {
return context.dataSources.users.findById(id);
},
users: async (_, { limit = 10, offset = 0 }, context) => {
return context.dataSources.users.findAll({ limit, offset });
},
me: async (_, __, context) => {
// Authenticated user from context
if (!context.user) {
throw new AuthenticationError('Not authenticated');
}
return context.user;
},
},
Mutation: {
createPost: async (_, { input }, context) => {
if (!context.user) {
throw new AuthenticationError('Not authenticated');
}
return context.dataSources.posts.create({
...input,
authorId: context.user.id,
});
},
deletePost: async (_, { id }, context) => {
const post = await context.dataSources.posts.findById(id);
// Authorization check
if (post.authorId !== context.user.id) {
throw new ForbiddenError('Not authorized');
}
await context.dataSources.posts.delete(id);
return true;
},
},
// Field resolvers - parent zawiera dane z parent type
User: {
// Resolver dla pola posts na typie User
posts: async (user, { first = 10 }, context) => {
// user = parent object (User)
return context.dataSources.posts.findByAuthorId(user.id, { first });
},
// Computed field
fullName: (user) => `${user.firstName} ${user.lastName}`,
},
Post: {
author: async (post, _, context) => {
// N+1 problem! Każdy post = osobne query
return context.dataSources.users.findById(post.authorId);
},
comments: async (post, _, context) => {
return context.dataSources.comments.findByPostId(post.id);
},
},
// Custom scalar
DateTime: new GraphQLScalarType({
name: 'DateTime',
description: 'ISO-8601 DateTime',
serialize: (value) => value.toISOString(),
parseValue: (value) => new Date(value),
parseLiteral: (ast) => new Date(ast.value),
}),
};
Resolver chain visualization:
query {
user(id: "1") { ← Query.user resolver
name ← Default resolver (user.name)
posts { ← User.posts resolver
title ← Default resolver (post.title)
author { ← Post.author resolver
name ← Default resolver (author.name)
}
}
}
}
Execution order:
1. Query.user(id: "1") → { id: "1", name: "John", ... }
2. User.posts(parent: user) → [{ id: "10", title: "...", authorId: "1" }]
3. Post.author(parent: post) → { id: "1", name: "John" } ← N+1!
3. Co to jest N+1 problem i jak go rozwiązać z DataLoader?
Odpowiedź:
N+1 problem to sytuacja gdy dla listy N elementów wykonujemy N+1 zapytań do bazy - 1 dla listy i N dla powiązanych danych każdego elementu.
┌─────────────────────────────────────────────────────────────────┐
│ N+1 PROBLEM │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Query: │
│ query { │
│ posts { # 1 query: SELECT * FROM posts │
│ title │
│ author { # N queries: SELECT * FROM users │
│ name # WHERE id = ? │
│ } # (dla każdego posta!) │
│ } │
│ } │
│ │
│ Bez DataLoader (10 postów = 11 queries): │
│ ───────────────────────────────────────── │
│ SELECT * FROM posts │
│ SELECT * FROM users WHERE id = 1 │
│ SELECT * FROM users WHERE id = 2 │
│ SELECT * FROM users WHERE id = 1 ← duplikat! │
│ SELECT * FROM users WHERE id = 3 │
│ ... (N razy) │
│ │
│ Z DataLoader (10 postów = 2 queries): │
│ ───────────────────────────────────── │
│ SELECT * FROM posts │
│ SELECT * FROM users WHERE id IN (1, 2, 3) ← batched! │
│ │
└─────────────────────────────────────────────────────────────────┘
DataLoader implementation:
const DataLoader = require('dataloader');
// Batch function - otrzymuje array keys, zwraca array results
// WAŻNE: Kolejność wyników musi odpowiadać kolejności keys!
const batchUsers = async (userIds) => {
console.log('Batch loading users:', userIds);
// userIds = [1, 2, 3, 1, 2] → unique = [1, 2, 3]
const users = await db.query(
'SELECT * FROM users WHERE id IN (?)',
[userIds]
);
// Map by id for O(1) lookup
const userMap = new Map(users.map(user => [user.id, user]));
// Return in same order as input keys
return userIds.map(id => userMap.get(id) || null);
};
// Create loader (per-request!)
const createLoaders = () => ({
userLoader: new DataLoader(batchUsers),
postLoader: new DataLoader(batchPosts),
commentLoader: new DataLoader(batchComments),
});
// Apollo Server setup
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => ({
user: getUserFromToken(req.headers.authorization),
loaders: createLoaders(), // Fresh loaders per request
}),
});
// Resolver using DataLoader
const resolvers = {
Post: {
author: (post, _, { loaders }) => {
// DataLoader batches & caches
return loaders.userLoader.load(post.authorId);
},
},
Comment: {
author: (comment, _, { loaders }) => {
// Same loader - will batch with Post.author
return loaders.userLoader.load(comment.authorId);
},
},
};
Jak DataLoader działa:
Event Loop Tick 1:
├─ Post.author resolver → userLoader.load(1)
├─ Post.author resolver → userLoader.load(2)
├─ Post.author resolver → userLoader.load(1) ← cached from above
└─ Post.author resolver → userLoader.load(3)
End of tick → DataLoader executes batch:
batchUsers([1, 2, 3]) ← deduplicated & batched
Event Loop Tick 2:
└─ Results returned to all waiting resolvers
DataLoader options:
new DataLoader(batchFn, {
// Cache within request (default: true)
cache: true,
// Max batch size
maxBatchSize: 100,
// Custom cache key function
cacheKeyFn: key => key.toString(),
// Batch scheduling (default: process.nextTick)
batchScheduleFn: callback => setTimeout(callback, 10),
});
4. Jak zaimplementować autentykację i autoryzację w GraphQL?
Odpowiedź:
Autentykacja i autoryzacja w GraphQL można zaimplementować na kilku poziomach:
// 1. CONTEXT - autentykacja przy każdym request
const server = new ApolloServer({
typeDefs,
resolvers,
context: async ({ req }) => {
// Extract token from header
const token = req.headers.authorization?.replace('Bearer ', '');
let user = null;
if (token) {
try {
// Verify JWT
const decoded = jwt.verify(token, process.env.JWT_SECRET);
user = await User.findById(decoded.userId);
} catch (err) {
// Invalid token - user remains null
}
}
return {
user,
loaders: createLoaders(),
};
},
});
// 2. RESOLVER-LEVEL authorization
const resolvers = {
Query: {
me: (_, __, { user }) => {
if (!user) {
throw new AuthenticationError('Must be logged in');
}
return user;
},
adminDashboard: (_, __, { user }) => {
if (!user) {
throw new AuthenticationError('Must be logged in');
}
if (user.role !== 'ADMIN') {
throw new ForbiddenError('Admin access required');
}
return getDashboardData();
},
},
Mutation: {
deleteUser: async (_, { id }, { user }) => {
if (!user) throw new AuthenticationError('Must be logged in');
if (user.role !== 'ADMIN' && user.id !== id) {
throw new ForbiddenError('Cannot delete other users');
}
return User.delete(id);
},
},
};
// 3. DIRECTIVE-BASED authorization (cleaner)
// Schema:
// directive @auth(requires: Role = USER) on FIELD_DEFINITION
//
// type Query {
// me: User @auth
// adminDashboard: Dashboard @auth(requires: ADMIN)
// publicPosts: [Post!]! # No directive = public
// }
const authDirective = (schema, directiveName) => {
return mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: (fieldConfig) => {
const directive = getDirective(schema, fieldConfig, directiveName)?.[0];
if (directive) {
const { requires = 'USER' } = directive;
const originalResolve = fieldConfig.resolve ?? defaultFieldResolver;
fieldConfig.resolve = async (source, args, context, info) => {
if (!context.user) {
throw new AuthenticationError('Must be logged in');
}
const roleHierarchy = ['USER', 'MODERATOR', 'ADMIN'];
const userRoleIndex = roleHierarchy.indexOf(context.user.role);
const requiredRoleIndex = roleHierarchy.indexOf(requires);
if (userRoleIndex < requiredRoleIndex) {
throw new ForbiddenError(`${requires} role required`);
}
return originalResolve(source, args, context, info);
};
}
return fieldConfig;
},
});
};
// 4. FIELD-LEVEL authorization (fine-grained)
const resolvers = {
User: {
email: (user, _, { user: currentUser }) => {
// Only show email to self or admin
if (currentUser?.id === user.id || currentUser?.role === 'ADMIN') {
return user.email;
}
return null; // or throw error
},
privateData: (user, _, { user: currentUser }) => {
if (currentUser?.id !== user.id) {
throw new ForbiddenError('Cannot access other user\'s private data');
}
return user.privateData;
},
},
};
Login mutation:
const resolvers = {
Mutation: {
login: async (_, { email, password }) => {
const user = await User.findByEmail(email);
if (!user || !await bcrypt.compare(password, user.passwordHash)) {
throw new AuthenticationError('Invalid credentials');
}
const token = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
return { token, user };
},
register: async (_, { input }) => {
const passwordHash = await bcrypt.hash(input.password, 10);
const user = await User.create({
...input,
passwordHash,
role: 'USER',
});
const token = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
return { token, user };
},
},
};
Queries i Mutations - Zaawansowane
5. Jak zaimplementować paginację w GraphQL?
Odpowiedź:
Dwa główne podejścia: offset-based i cursor-based (Relay specification).
# Offset-based (proste, ale problemy ze zmianami danych)
type Query {
posts(limit: Int = 10, offset: Int = 0): [Post!]!
}
# Cursor-based (Relay spec - stabilne, zalecane)
type Query {
posts(first: Int, after: String, last: Int, before: String): PostConnection!
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type PostEdge {
node: Post!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
Implementacja cursor-based pagination:
const resolvers = {
Query: {
posts: async (_, { first = 10, after, last, before }, { db }) => {
// Decode cursor (base64 encoded id or timestamp)
const decodeCursor = (cursor) => {
if (!cursor) return null;
return Buffer.from(cursor, 'base64').toString('utf8');
};
const encodeCursor = (id) => {
return Buffer.from(id.toString()).toString('base64');
};
let query = db('posts').orderBy('created_at', 'desc');
// Forward pagination (first + after)
if (after) {
const afterId = decodeCursor(after);
const afterPost = await db('posts').where('id', afterId).first();
query = query.where('created_at', '<', afterPost.created_at);
}
// Backward pagination (last + before)
if (before) {
const beforeId = decodeCursor(before);
const beforePost = await db('posts').where('id', beforeId).first();
query = query.where('created_at', '>', beforePost.created_at);
}
// Fetch one extra to determine hasNextPage
const limit = first || last || 10;
const posts = await query.limit(limit + 1);
const hasMore = posts.length > limit;
const nodes = hasMore ? posts.slice(0, -1) : posts;
// Reverse for backward pagination
if (last) {
nodes.reverse();
}
const edges = nodes.map(post => ({
node: post,
cursor: encodeCursor(post.id),
}));
const totalCount = await db('posts').count('* as count').first();
return {
edges,
pageInfo: {
hasNextPage: first ? hasMore : false,
hasPreviousPage: last ? hasMore : (after ? true : false),
startCursor: edges[0]?.cursor,
endCursor: edges[edges.length - 1]?.cursor,
},
totalCount: totalCount.count,
};
},
},
};
Client usage:
# First page
query {
posts(first: 10) {
edges {
node { id, title }
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
# Next page
query {
posts(first: 10, after: "YWJjMTIz") {
edges {
node { id, title }
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
Offset vs Cursor comparison:
| Aspekt | Offset | Cursor |
|---|---|---|
| Implementacja | Prosta | Złożona |
| Performance | O(n) skip | O(1) seek |
| Stabilność | Problemy przy insert/delete | Stabilna |
| Random access | Tak (page 50) | Nie (sekwencyjnie) |
| Use case | Małe datasety | Duże datasety, infinite scroll |
6. Jak obsłużyć błędy w GraphQL?
Odpowiedź:
GraphQL ma specyficzny sposób obsługi błędów - partial responses i errors array:
// Response format
{
"data": {
"user": {
"name": "John",
"posts": null // Partial failure
}
},
"errors": [
{
"message": "Failed to fetch posts",
"path": ["user", "posts"],
"extensions": {
"code": "INTERNAL_SERVER_ERROR"
}
}
]
}
Custom error classes:
const { ApolloError, UserInputError, AuthenticationError, ForbiddenError } = require('apollo-server');
// Custom error
class NotFoundError extends ApolloError {
constructor(resource, id) {
super(`${resource} with id ${id} not found`, 'NOT_FOUND', {
resource,
id,
});
}
}
class ValidationError extends ApolloError {
constructor(errors) {
super('Validation failed', 'VALIDATION_ERROR', {
validationErrors: errors,
});
}
}
// Usage in resolvers
const resolvers = {
Query: {
user: async (_, { id }, { db }) => {
const user = await db.users.findById(id);
if (!user) {
throw new NotFoundError('User', id);
}
return user;
},
},
Mutation: {
createPost: async (_, { input }, { user }) => {
// Validation
const errors = [];
if (!input.title || input.title.length < 3) {
errors.push({ field: 'title', message: 'Title must be at least 3 characters' });
}
if (input.content && input.content.length > 10000) {
errors.push({ field: 'content', message: 'Content too long' });
}
if (errors.length > 0) {
throw new ValidationError(errors);
}
// Business logic error
if (await hasReachedPostLimit(user.id)) {
throw new ApolloError(
'Post limit reached',
'POST_LIMIT_EXCEEDED',
{ limit: 100, current: 100 }
);
}
return createPost(input);
},
},
};
Error formatting:
const server = new ApolloServer({
typeDefs,
resolvers,
formatError: (error) => {
// Log error for monitoring
console.error('GraphQL Error:', error);
// Don't expose internal errors in production
if (process.env.NODE_ENV === 'production') {
if (error.extensions?.code === 'INTERNAL_SERVER_ERROR') {
return {
message: 'An unexpected error occurred',
extensions: { code: 'INTERNAL_SERVER_ERROR' },
};
}
}
// Return full error in development
return error;
},
});
Union type for errors (alternative pattern):
type Query {
user(id: ID!): UserResult!
}
union UserResult = User | NotFoundError | PermissionError
type NotFoundError {
message: String!
id: ID!
}
type PermissionError {
message: String!
requiredRole: Role!
}
const resolvers = {
Query: {
user: async (_, { id }, { user: currentUser, db }) => {
const user = await db.users.findById(id);
if (!user) {
return {
__typename: 'NotFoundError',
message: `User ${id} not found`,
id,
};
}
if (user.isPrivate && currentUser?.id !== id) {
return {
__typename: 'PermissionError',
message: 'Cannot view private profile',
requiredRole: 'ADMIN',
};
}
return { __typename: 'User', ...user };
},
},
UserResult: {
__resolveType: (obj) => obj.__typename,
},
};
7. Jak działa Subscriptions w GraphQL?
Odpowiedź:
Subscriptions to operacje GraphQL dla real-time danych, implementowane przez WebSocket.
type Subscription {
postCreated: Post!
commentAdded(postId: ID!): Comment!
userStatusChanged(userId: ID!): UserStatus!
}
type UserStatus {
userId: ID!
isOnline: Boolean!
lastSeen: DateTime
}
Server implementation (Apollo Server + graphql-ws):
const { ApolloServer } = require('@apollo/server');
const { expressMiddleware } = require('@apollo/server/express4');
const { createServer } = require('http');
const { WebSocketServer } = require('ws');
const { useServer } = require('graphql-ws/lib/use/ws');
const { makeExecutableSchema } = require('@graphql-tools/schema');
const { PubSub } = require('graphql-subscriptions');
// PubSub for local development
// Use Redis PubSub for production (multiple servers)
const pubsub = new PubSub();
// Event names
const EVENTS = {
POST_CREATED: 'POST_CREATED',
COMMENT_ADDED: 'COMMENT_ADDED',
USER_STATUS_CHANGED: 'USER_STATUS_CHANGED',
};
const resolvers = {
Mutation: {
createPost: async (_, { input }, { user, db }) => {
const post = await db.posts.create({ ...input, authorId: user.id });
// Publish event
pubsub.publish(EVENTS.POST_CREATED, { postCreated: post });
return post;
},
addComment: async (_, { postId, text }, { user, db }) => {
const comment = await db.comments.create({
postId,
text,
authorId: user.id,
});
// Publish to specific channel
pubsub.publish(`${EVENTS.COMMENT_ADDED}.${postId}`, {
commentAdded: comment,
});
return comment;
},
},
Subscription: {
postCreated: {
// Subscribe returns AsyncIterator
subscribe: () => pubsub.asyncIterator([EVENTS.POST_CREATED]),
},
commentAdded: {
subscribe: (_, { postId }) => {
// Filter by postId
return pubsub.asyncIterator([`${EVENTS.COMMENT_ADDED}.${postId}`]);
},
},
userStatusChanged: {
subscribe: withFilter(
() => pubsub.asyncIterator([EVENTS.USER_STATUS_CHANGED]),
(payload, variables) => {
// Only send to subscribers watching this user
return payload.userStatusChanged.userId === variables.userId;
}
),
},
},
};
// Server setup with WebSocket
const schema = makeExecutableSchema({ typeDefs, resolvers });
const app = express();
const httpServer = createServer(app);
// WebSocket server
const wsServer = new WebSocketServer({
server: httpServer,
path: '/graphql',
});
// Setup graphql-ws
const serverCleanup = useServer(
{
schema,
context: async (ctx) => {
// Auth for WebSocket
const token = ctx.connectionParams?.authToken;
const user = token ? await verifyToken(token) : null;
return { user, pubsub };
},
onConnect: async (ctx) => {
console.log('Client connected');
// Validate connection
if (!ctx.connectionParams?.authToken) {
return false; // Reject connection
}
return true;
},
onDisconnect: (ctx) => {
console.log('Client disconnected');
},
},
wsServer
);
// Apollo Server
const server = new ApolloServer({
schema,
plugins: [
{
async serverWillStart() {
return {
async drainServer() {
await serverCleanup.dispose();
},
};
},
},
],
});
await server.start();
app.use('/graphql', expressMiddleware(server));
httpServer.listen(4000);
Client usage (Apollo Client):
import { useSubscription, gql } from '@apollo/client';
const COMMENT_SUBSCRIPTION = gql`
subscription OnCommentAdded($postId: ID!) {
commentAdded(postId: $postId) {
id
text
author {
name
}
}
}
`;
function Comments({ postId }) {
const { data, loading } = useSubscription(COMMENT_SUBSCRIPTION, {
variables: { postId },
});
if (data?.commentAdded) {
// New comment received!
console.log('New comment:', data.commentAdded);
}
return <div>...</div>;
}
Production considerations:
// Use Redis PubSub for multi-server setup
const { RedisPubSub } = require('graphql-redis-subscriptions');
const Redis = require('ioredis');
const pubsub = new RedisPubSub({
publisher: new Redis(process.env.REDIS_URL),
subscriber: new Redis(process.env.REDIS_URL),
});
Wydajność i Caching
8. Jak zaimplementować caching w GraphQL?
Odpowiedź:
Caching w GraphQL jest trudniejsze niż w REST (brak HTTP cache). Strategie:
// 1. DataLoader caching (per-request, automatic)
const userLoader = new DataLoader(batchUsers);
// Subsequent calls to userLoader.load(1) return cached value
// 2. Application-level cache (Redis)
const Redis = require('ioredis');
const redis = new Redis();
const resolvers = {
Query: {
post: async (_, { id }, { redis }) => {
// Check cache first
const cacheKey = `post:${id}`;
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Fetch from DB
const post = await db.posts.findById(id);
// Cache for 5 minutes
await redis.setex(cacheKey, 300, JSON.stringify(post));
return post;
},
},
Mutation: {
updatePost: async (_, { id, input }, { redis }) => {
const post = await db.posts.update(id, input);
// Invalidate cache
await redis.del(`post:${id}`);
return post;
},
},
};
// 3. Apollo Server cache hints (CDN caching)
const resolvers = {
Query: {
posts: (_, __, ___, info) => {
// Set cache hint
info.cacheControl.setCacheHint({ maxAge: 60, scope: 'PUBLIC' });
return db.posts.findAll();
},
},
Post: {
__resolveReference: (post, _, info) => {
info.cacheControl.setCacheHint({ maxAge: 300, scope: 'PUBLIC' });
return db.posts.findById(post.id);
},
},
};
// Schema directives for cache
// type Post @cacheControl(maxAge: 300) {
// id: ID!
// title: String!
// viewCount: Int! @cacheControl(maxAge: 0) # No cache for dynamic fields
// }
// 4. Persisted Queries (reduce bandwidth, enable CDN)
const { ApolloServerPluginCacheControl } = require('@apollo/server/plugin/cacheControl');
const responseCachePlugin = require('@apollo/server-plugin-response-cache').default;
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
ApolloServerPluginCacheControl({ defaultMaxAge: 60 }),
responseCachePlugin(),
],
});
Apollo Client caching:
import { ApolloClient, InMemoryCache } from '@apollo/client';
const client = new ApolloClient({
uri: '/graphql',
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
posts: {
// Merge paginated results
keyArgs: ['filter'],
merge(existing = [], incoming) {
return [...existing, ...incoming];
},
},
},
},
Post: {
// How to identify Post objects
keyFields: ['id'],
},
},
}),
});
// Fetch policy options
const { data } = useQuery(GET_POSTS, {
fetchPolicy: 'cache-first', // Default: cache then network
// fetchPolicy: 'network-only', // Always fetch
// fetchPolicy: 'cache-only', // Only cache
// fetchPolicy: 'no-cache', // Fetch, don't cache
// fetchPolicy: 'cache-and-network', // Cache immediately, then update
});
9. Jak zabezpieczyć GraphQL przed nadużyciami?
Odpowiedź:
GraphQL jest podatny na kilka rodzajów ataków. Kluczowe zabezpieczenia:
// 1. Query Complexity Analysis
const { createComplexityLimitRule } = require('graphql-validation-complexity');
const complexityLimit = createComplexityLimitRule(1000, {
scalarCost: 1,
objectCost: 2,
listFactor: 10,
});
// Or use graphql-query-complexity
const { getComplexity, simpleEstimator } = require('graphql-query-complexity');
const server = new ApolloServer({
validationRules: [complexityLimit],
plugins: [{
requestDidStart: () => ({
didResolveOperation({ request, document }) {
const complexity = getComplexity({
schema,
query: document,
variables: request.variables,
estimators: [simpleEstimator({ defaultComplexity: 1 })],
});
if (complexity > 1000) {
throw new Error(`Query too complex: ${complexity} > 1000`);
}
},
}),
}],
});
// 2. Query Depth Limiting
const depthLimit = require('graphql-depth-limit');
const server = new ApolloServer({
validationRules: [depthLimit(10)], // Max 10 levels deep
});
// 3. Rate Limiting
const rateLimit = require('express-rate-limit');
const graphqlRateLimit = require('graphql-rate-limit');
// Express-level rate limit
app.use('/graphql', rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
}));
// Field-level rate limit
const rateLimitDirective = graphqlRateLimit.createRateLimitDirective({
identifyContext: (ctx) => ctx.user?.id || ctx.ip,
});
// type Query {
// expensiveQuery: Data @rateLimit(window: "1m", max: 10)
// }
// 4. Persisted Queries (whitelist)
const server = new ApolloServer({
persistedQueries: {
cache: new RedisCache(),
},
});
// In production, disable ad-hoc queries
if (process.env.NODE_ENV === 'production') {
server.plugins.push({
requestDidStart() {
return {
didResolveOperation({ request }) {
if (!request.extensions?.persistedQuery) {
throw new Error('Only persisted queries allowed');
}
},
};
},
});
}
// 5. Input Validation
const resolvers = {
Mutation: {
createPost: async (_, { input }) => {
// Size limits
if (input.title.length > 200) {
throw new UserInputError('Title too long');
}
if (input.content.length > 50000) {
throw new UserInputError('Content too long');
}
// Sanitize HTML
const sanitizedContent = sanitizeHtml(input.content);
return createPost({ ...input, content: sanitizedContent });
},
},
};
// 6. Disable Introspection in Production
const { ApolloServerPluginLandingPageDisabled } = require('@apollo/server/plugin/disabled');
const server = new ApolloServer({
introspection: process.env.NODE_ENV !== 'production',
plugins: process.env.NODE_ENV === 'production'
? [ApolloServerPluginLandingPageDisabled()]
: [],
});
Query complexity example:
# High complexity query (attack vector)
query EvilQuery {
users(first: 100) { # 100 users
posts(first: 100) { # × 100 posts each
comments(first: 100) { # × 100 comments each
author { # × author for each
posts(first: 100) { # × 100 posts... infinite!
title
}
}
}
}
}
}
# Complexity: 100 × 100 × 100 × 100 = 100,000,000!
# Blocked by depth limit (depth > 10) and complexity limit
Federation i Microservices
10. Co to jest Apollo Federation i kiedy go używać?
Odpowiedź:
Apollo Federation pozwala budować distributed GraphQL z wielu microservices:
┌─────────────────────────────────────────────────────────────────┐
│ APOLLO FEDERATION │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ │
│ │ Apollo Gateway │ │
│ │ (Supergraph) │ │
│ └────────┬─────────┘ │
│ │ │
│ ┌───────────────────┼───────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Users │ │ Posts │ │ Comments │ │
│ │ Subgraph │ │ Subgraph │ │ Subgraph │ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ │
│ Każdy subgraph to osobny serwis z własnym schema │
│ Gateway łączy je w jeden unified API │
└─────────────────────────────────────────────────────────────────┘
Users Subgraph:
# users-subgraph/schema.graphql
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0",
import: ["@key", "@shareable"])
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
}
type Query {
user(id: ID!): User
me: User
}
Posts Subgraph (extends User):
# posts-subgraph/schema.graphql
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0",
import: ["@key", "@external"])
type Post @key(fields: "id") {
id: ID!
title: String!
content: String
author: User!
}
# Extend User type from users-subgraph
type User @key(fields: "id") {
id: ID! @external
posts: [Post!]! # Add posts field to User
}
type Query {
post(id: ID!): Post
posts: [Post!]!
}
// posts-subgraph/resolvers.js
const resolvers = {
User: {
// Resolve posts for User entity from users-subgraph
posts: (user) => {
return db.posts.findByAuthorId(user.id);
},
},
Post: {
// Reference resolver - resolve User from just { id }
author: (post) => {
return { __typename: 'User', id: post.authorId };
},
},
};
Gateway setup:
const { ApolloServer } = require('@apollo/server');
const { ApolloGateway } = require('@apollo/gateway');
const gateway = new ApolloGateway({
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: 'users', url: 'http://users-service:4001/graphql' },
{ name: 'posts', url: 'http://posts-service:4002/graphql' },
{ name: 'comments', url: 'http://comments-service:4003/graphql' },
],
}),
});
const server = new ApolloServer({ gateway });
Federation directives:
| Directive | Purpose |
|---|---|
@key |
Define entity's primary key |
@external |
Reference field from another subgraph |
@provides |
Specify fields resolved by this subgraph |
@requires |
Fields needed to resolve this field |
@shareable |
Multiple subgraphs can resolve this field |
@override |
Take ownership of field from another subgraph |
When to use Federation:
- Multiple teams own different domains
- Microservices architecture
- Need independent deployment
- Large schema that should be split
When NOT to use:
- Small team / monolith
- Simple CRUD application
- All data in one database
- Starting new project (add complexity later)
Praktyczne Zadania
Zadanie 1: Debug slow query
Query posts { author { posts { comments }}} jest wolne. Jak to zbadasz?
Podejście:
// 1. Check for N+1
// Add query logging
const db = knex({
client: 'pg',
debug: true, // Log all queries
});
// 2. Add DataLoader if missing
const authorLoader = new DataLoader(async (authorIds) => {
console.log('Batch loading authors:', authorIds);
const authors = await db('users').whereIn('id', authorIds);
return authorIds.map(id => authors.find(a => a.id === id));
});
// 3. Add query complexity limit
// posts(100) → author(100) → posts(100×100) = too much
// 4. Consider denormalization
// Cache author data in post record
Zadanie 2: Implement optimistic locking
Zaimplementuj mutation z optimistic locking dla concurrent updates.
Rozwiązanie:
type Post {
id: ID!
title: String!
version: Int! # For optimistic locking
}
input UpdatePostInput {
title: String
expectedVersion: Int! # Client sends current version
}
const resolvers = {
Mutation: {
updatePost: async (_, { id, input }, { db }) => {
const { expectedVersion, ...updates } = input;
const result = await db('posts')
.where({ id, version: expectedVersion })
.update({
...updates,
version: db.raw('version + 1'),
})
.returning('*');
if (result.length === 0) {
throw new ApolloError(
'Post was modified by another user',
'CONCURRENT_MODIFICATION',
{ expectedVersion }
);
}
return result[0];
},
},
};
Zadanie 3: Design a GraphQL API for e-commerce
Rozwiązanie:
type Query {
products(filter: ProductFilter, first: Int, after: String): ProductConnection!
product(id: ID!): Product
cart: Cart
orders(first: Int, after: String): OrderConnection!
}
type Mutation {
addToCart(productId: ID!, quantity: Int!): Cart!
removeFromCart(itemId: ID!): Cart!
checkout(input: CheckoutInput!): Order!
cancelOrder(id: ID!): Order!
}
type Subscription {
orderStatusChanged(orderId: ID!): OrderStatus!
}
type Product @key(fields: "id") {
id: ID!
name: String!
price: Money!
inventory: Int!
category: Category!
reviews(first: Int): ReviewConnection!
}
type Cart {
id: ID!
items: [CartItem!]!
total: Money!
}
type Order {
id: ID!
items: [OrderItem!]!
status: OrderStatus!
total: Money!
createdAt: DateTime!
}
Podsumowanie
GraphQL to potężne narzędzie dla nowoczesnych API. Na rozmowie rekrutacyjnej oczekuj pytań o:
- Podstawy - różnica vs REST, schema, typy, resolvers
- N+1 problem - DataLoader, batching, caching
- Autentykacja - JWT, context, directives
- Subscriptions - WebSocket, PubSub, real-time
- Performance - caching, complexity limits, security
- Federation - microservices, distributed schema
Klucz do sukcesu: zrozumienie trade-offs między GraphQL a REST, i znajomość rozwiązań typowych problemów (N+1, security, caching).
Zobacz też
Artykuł przygotowany przez zespół Flipcards - tworzymy materiały do nauki programowania i przygotowania do rozmów rekrutacyjnych.
Chcesz więcej pytań rekrutacyjnych?
To tylko jeden temat z naszego kompletnego przewodnika po rozmowach rekrutacyjnych. Uzyskaj dostęp do 800+ pytań z 13 technologii.
