GraphQL¶
What is GraphQL?¶
Query language for APIs that allows clients to request exactly the data they need.
Schema Definition Language (SDL)¶
Types¶
# Scalar types (built-in)
# String, Int, Float, Boolean, ID
# Object type
type User {
id: ID!
email: String!
name: String
age: Int
isActive: Boolean!
createdAt: DateTime!
# Relations
orders: [Order!]!
address: Address
}
type Order {
id: ID!
orderNumber: String!
status: OrderStatus!
total: Float!
items: [OrderItem!]!
customer: User!
createdAt: DateTime!
}
type OrderItem {
id: ID!
product: Product!
quantity: Int!
unitPrice: Float!
subtotal: Float!
}
type Address {
street: String!
city: String!
state: String
postalCode: String!
country: String!
}
# Enum type
enum OrderStatus {
PENDING
CONFIRMED
SHIPPED
DELIVERED
CANCELLED
}
# Input type (for mutations)
input CreateOrderInput {
customerId: ID!
items: [OrderItemInput!]!
shippingAddress: AddressInput!
}
input OrderItemInput {
productId: ID!
quantity: Int!
}
input AddressInput {
street: String!
city: String!
state: String
postalCode: String!
country: String!
}
# Interface
interface Node {
id: ID!
}
type User implements Node {
id: ID!
# ... other fields
}
# Union type
union SearchResult = User | Product | Order
# Custom scalar
scalar DateTime
scalar JSON
Schema Entry Points¶
type Query {
# Single item
user(id: ID!): User
order(id: ID!): Order
# Lists with pagination
users(first: Int, after: String): UserConnection!
orders(
status: OrderStatus
customerId: ID
first: Int
after: String
): OrderConnection!
# Search
search(query: String!): [SearchResult!]!
}
type Mutation {
createUser(input: CreateUserInput!): CreateUserPayload!
updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload!
deleteUser(id: ID!): DeleteUserPayload!
createOrder(input: CreateOrderInput!): CreateOrderPayload!
cancelOrder(id: ID!, reason: String): CancelOrderPayload!
}
type Subscription {
orderStatusChanged(orderId: ID!): Order!
newOrder(customerId: ID): Order!
}
schema {
query: Query
mutation: Mutation
subscription: Subscription
}
Queries¶
Basic Queries¶
# Simple query
query GetUser {
user(id: "123") {
id
name
email
}
}
# Query with variables
query GetUser($userId: ID!) {
user(id: $userId) {
id
name
email
orders {
id
total
status
}
}
}
# Variables (sent with query)
{
"userId": "123"
}
# Multiple queries in one request
query GetUserAndOrders($userId: ID!) {
user(id: $userId) {
name
email
}
recentOrders: orders(customerId: $userId, first: 5) {
edges {
node {
id
total
}
}
}
}
Fragments¶
# Reusable field selection
fragment UserBasic on User {
id
name
email
}
fragment OrderSummary on Order {
id
orderNumber
status
total
}
query GetOrderWithUsers($orderId: ID!) {
order(id: $orderId) {
...OrderSummary
customer {
...UserBasic
}
items {
product {
name
price
}
quantity
}
}
}
# Inline fragments (for unions/interfaces)
query Search($query: String!) {
search(query: $query) {
... on User {
name
email
}
... on Product {
name
price
}
... on Order {
orderNumber
total
}
}
}
Directives¶
query GetUser($userId: ID!, $includeOrders: Boolean!, $skipEmail: Boolean!) {
user(id: $userId) {
id
name
email @skip(if: $skipEmail)
orders @include(if: $includeOrders) {
id
total
}
}
}
# Built-in directives
@skip(if: Boolean!) # Skip field if true
@include(if: Boolean!) # Include field if true
@deprecated(reason: String) # Mark as deprecated
Mutations¶
# Create
mutation CreateOrder($input: CreateOrderInput!) {
createOrder(input: $input) {
order {
id
orderNumber
status
total
}
errors {
field
message
}
}
}
# Update
mutation UpdateOrderStatus($orderId: ID!, $status: OrderStatus!) {
updateOrderStatus(id: $orderId, status: $status) {
order {
id
status
updatedAt
}
errors {
message
}
}
}
# Delete
mutation CancelOrder($orderId: ID!, $reason: String) {
cancelOrder(id: $orderId, reason: $reason) {
success
order {
id
status
}
errors {
message
}
}
}
# Variables
{
"input": {
"customerId": "cust_123",
"items": [
{ "productId": "prod_456", "quantity": 2 }
],
"shippingAddress": {
"street": "123 Main St",
"city": "New York",
"postalCode": "10001",
"country": "US"
}
}
}
Mutation Response Pattern¶
# Payload types for mutations
type CreateOrderPayload {
order: Order
errors: [UserError!]!
clientMutationId: String # For client-side tracking
}
type UserError {
field: [String!] # Path to field with error
message: String!
code: String
}
# Usage ensures type-safe error handling
mutation CreateOrder($input: CreateOrderInput!) {
createOrder(input: $input) {
order {
id
}
errors {
field
message
code
}
}
}
Subscriptions¶
# Schema
type Subscription {
orderStatusChanged(orderId: ID!): OrderStatusUpdate!
newOrders: Order!
}
type OrderStatusUpdate {
order: Order!
previousStatus: OrderStatus!
newStatus: OrderStatus!
changedAt: DateTime!
}
# Client subscription
subscription WatchOrder($orderId: ID!) {
orderStatusChanged(orderId: $orderId) {
order {
id
status
}
previousStatus
newStatus
changedAt
}
}
Pagination¶
Cursor-Based (Relay Connection Spec)¶
# Schema
type Query {
orders(
first: Int
after: String
last: Int
before: String
filter: OrderFilter
): OrderConnection!
}
type OrderConnection {
edges: [OrderEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type OrderEdge {
node: Order!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
# Query
query GetOrders($after: String) {
orders(first: 10, after: $after) {
edges {
node {
id
orderNumber
total
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
totalCount
}
}
Offset-Based (Simpler)¶
type Query {
orders(
offset: Int = 0
limit: Int = 20
filter: OrderFilter
): OrderList!
}
type OrderList {
items: [Order!]!
total: Int!
hasMore: Boolean!
}
Java Implementation (Spring GraphQL)¶
Schema-First Approach¶
// Controller
@Controller
public class OrderController {
private final OrderService orderService;
// Query resolver
@QueryMapping
public Order order(@Argument String id) {
return orderService.findById(id)
.orElseThrow(() -> new OrderNotFoundException(id));
}
@QueryMapping
public Connection<Order> orders(
@Argument OrderFilter filter,
@Argument int first,
@Argument String after) {
return orderService.findAll(filter, first, after);
}
// Mutation resolver
@MutationMapping
public CreateOrderPayload createOrder(@Argument CreateOrderInput input) {
try {
Order order = orderService.create(input);
return CreateOrderPayload.success(order);
} catch (ValidationException e) {
return CreateOrderPayload.error(e.getErrors());
}
}
// Field resolver (for relations)
@SchemaMapping(typeName = "Order", field = "customer")
public User customer(Order order) {
return userService.findById(order.getCustomerId());
}
@SchemaMapping(typeName = "Order", field = "items")
public List<OrderItem> items(Order order) {
return orderItemService.findByOrderId(order.getId());
}
}
// Subscription
@Controller
public class OrderSubscriptionController {
@SubscriptionMapping
public Flux<Order> orderStatusChanged(@Argument String orderId) {
return orderEventPublisher.subscribe(orderId);
}
}
DataLoader (N+1 Solution)¶
@Configuration
public class DataLoaderConfig {
@Bean
public BatchLoaderRegistry batchLoaderRegistry(UserService userService) {
return registry -> {
registry.forTypePair(String.class, User.class)
.registerMappedBatchLoader((userIds, env) -> {
// Batch load users
Map<String, User> users = userService.findByIds(userIds);
return Mono.just(users);
});
};
}
}
@Controller
public class OrderController {
@SchemaMapping(typeName = "Order", field = "customer")
public CompletableFuture<User> customer(
Order order,
DataLoader<String, User> userLoader) {
return userLoader.load(order.getCustomerId());
}
}
N+1 Problem¶
Error Handling¶
# Standard GraphQL error format
{
"data": {
"createOrder": null
},
"errors": [
{
"message": "Insufficient inventory for product prod_123",
"locations": [{ "line": 2, "column": 3 }],
"path": ["createOrder"],
"extensions": {
"code": "INSUFFICIENT_INVENTORY",
"productId": "prod_123",
"available": 5,
"requested": 10
}
}
]
}
# Application-level errors in response
{
"data": {
"createOrder": {
"order": null,
"errors": [
{
"field": ["input", "items", "0", "quantity"],
"message": "Insufficient inventory",
"code": "INSUFFICIENT_INVENTORY"
}
]
}
}
}
Java Error Handling¶
@ControllerAdvice
public class GraphQLExceptionHandler {
@ExceptionHandler(OrderNotFoundException.class)
public GraphQLError handleNotFound(OrderNotFoundException ex) {
return GraphQLError.newError()
.message(ex.getMessage())
.errorType(ErrorType.NOT_FOUND)
.extensions(Map.of(
"code", "ORDER_NOT_FOUND",
"orderId", ex.getOrderId()
))
.build();
}
@ExceptionHandler(ValidationException.class)
public GraphQLError handleValidation(ValidationException ex) {
return GraphQLError.newError()
.message("Validation failed")
.errorType(ErrorType.BAD_REQUEST)
.extensions(Map.of(
"code", "VALIDATION_ERROR",
"errors", ex.getErrors()
))
.build();
}
}
Security¶
Authentication & Authorization¶
@Controller
public class OrderController {
@QueryMapping
@PreAuthorize("isAuthenticated()")
public Order order(@Argument String id) {
return orderService.findById(id);
}
@MutationMapping
@PreAuthorize("hasRole('ADMIN') or @orderSecurity.isOwner(#id)")
public CancelOrderPayload cancelOrder(@Argument String id) {
return orderService.cancel(id);
}
}
// Field-level authorization
@SchemaMapping(typeName = "User", field = "email")
@PreAuthorize("hasRole('ADMIN') or #user.id == authentication.principal.id")
public String email(User user) {
return user.getEmail();
}
Query Complexity & Depth Limiting¶
@Configuration
public class GraphQLSecurityConfig {
@Bean
public Instrumentation maxQueryDepthInstrumentation() {
return new MaxQueryDepthInstrumentation(10);
}
@Bean
public Instrumentation maxQueryComplexityInstrumentation() {
return new MaxQueryComplexityInstrumentation(100);
}
}
// Schema complexity directives
type Query {
orders(first: Int): [Order!]! @complexity(value: 10, multiplier: "first")
}
GraphQL vs REST¶
| Aspect | GraphQL | REST |
|---|---|---|
| Endpoints | Single (/graphql) | Multiple |
| Data fetching | Client specifies | Server determines |
| Over-fetching | No | Common |
| Under-fetching | No | Common |
| Versioning | Schema evolution | URL/header versioning |
| Caching | Complex | HTTP caching built-in |
| File upload | Not native | Native |
| Learning curve | Steeper | Lower |
| Tooling | Growing | Mature |
When to use GraphQL: - Complex, nested data requirements - Multiple clients with different needs - Rapid frontend iteration - Mobile apps (bandwidth concerns)
When to use REST: - Simple CRUD operations - Public APIs (caching important) - File operations - Team unfamiliar with GraphQL
Common Interview Questions¶
- What is GraphQL?
- Query language for APIs
- Client specifies exactly what data needed
-
Single endpoint, flexible queries
-
How to solve N+1 problem?
- DataLoader pattern
-
Batch and cache database queries
-
GraphQL vs REST?
- GraphQL: Flexible queries, single endpoint
-
REST: Multiple endpoints, HTTP caching
-
How to handle errors?
- Standard errors array
- Application-level errors in response payload
-
Extensions for additional context
-
Security concerns?
- Query depth/complexity limiting
- Authentication/authorization
- Rate limiting
- Disable introspection in production