Skip to content

GraphQL

What is GraphQL?

Query language for APIs that allows clients to request exactly the data they need.

GraphQL Overview


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

N+1 Problem in GraphQL


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

  1. What is GraphQL?
  2. Query language for APIs
  3. Client specifies exactly what data needed
  4. Single endpoint, flexible queries

  5. How to solve N+1 problem?

  6. DataLoader pattern
  7. Batch and cache database queries

  8. GraphQL vs REST?

  9. GraphQL: Flexible queries, single endpoint
  10. REST: Multiple endpoints, HTTP caching

  11. How to handle errors?

  12. Standard errors array
  13. Application-level errors in response payload
  14. Extensions for additional context

  15. Security concerns?

  16. Query depth/complexity limiting
  17. Authentication/authorization
  18. Rate limiting
  19. Disable introspection in production