Backend & APIs Topic

GraphQL - Modern API Query Language

What You'll Learn:

  • Understanding GraphQL concepts and advantages over REST

  • Implementing GraphQL in Spring Boot applications

  • Schema design and type system

  • Queries, mutations, and subscriptions

  • Advanced features and best practices

Detailed Explanation:

What is GraphQL?

GraphQL is a query language for APIs and a runtime for executing those queries by using a type system you define for your data. It was developed by Facebook in 2012 and open-sourced in 2015.

Key Characteristics:

  • Client-specified queries: Clients request exactly what they need

  • Single endpoint: All operations go through one endpoint

  • Strongly typed: Type system defines API capabilities

  • Introspective: Clients can discover schema

  • Real-time: Subscriptions for real-time updates

GraphQL vs REST Comparison

Aspect REST GraphQL
Endpoints Multiple endpoints Single endpoint
Over-fetching Common Eliminated
Under-fetching Common Eliminated
Versioning URL/header versioning Schema evolution
Caching HTTP caching Custom caching
Learning Curve Lower Higher
Tooling Mature Growing

GraphQL Core Concepts

1. Schema Definition Language (SDL):

type User {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!
  createdAt: String!
}

type Post {
  id: ID!
  title: String!
  content: String!
  author: User!
  comments: [Comment!]!
}

type Comment {
  id: ID!
  content: String!
  author: User!
  post: Post!
}

type Query {
  users: [User!]!
  user(id: ID!): User
  posts: [Post!]!
  post(id: ID!): Post
}

type Mutation {
  createUser(name: String!, email: String!): User!
  createPost(title: String!, content: String!, authorId: ID!): Post!
}

type Subscription {
  postAdded: Post!
}

2. Basic Query Examples:

# Get all users with their names and emails
query {
  users {
    id
    name
    email
  }
}

# Get specific user with their posts
query {
  user(id: "1") {
    name
    email
    posts {
      title
      content
    }
  }
}

Spring Boot GraphQL Implementation

Dependencies:

xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-graphql</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-java-extended-scalars</artifactId>
    <version>20.0</version>
</dependency>

Configuration:

properties

# GraphQL endpoint
spring.graphql.path=/graphql
spring.graphql.graphiql.enabled=true
spring.graphql.schema.printer.enabled=true

# CORS configuration
spring.graphql.cors.allowed-origins=*
spring.graphql.cors.allowed-methods=GET,POST

Entity Classes

User Entity:

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    private String email;
    
    @OneToMany(mappedBy = "author", cascade = CascadeType.ALL)
    private List<Post> posts = new ArrayList<>();
    
    @CreationTimestamp
    private LocalDateTime createdAt;
    
    // Constructors, getters, setters
    public User() {}
    
    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }
}

Post Entity:

@Entity
@Table(name = "posts")
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String title;
    private String content;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "author_id")
    private User author;
    
    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL)
    private List<Comment> comments = new ArrayList<>();
    
    @CreationTimestamp
    private LocalDateTime createdAt;
    
    // Constructors, getters, setters
}

GraphQL Controllers (DataFetchers)

1. Query Resolvers:

@Component
public class UserQueryResolver implements GraphQLQueryResolver {
    
    private final UserRepository userRepository;
    private final PostRepository postRepository;
    
    public UserQueryResolver(UserRepository userRepository, PostRepository postRepository) {
        this.userRepository = userRepository;
        this.postRepository = postRepository;
    }
    
    public List<User> users() {
        return userRepository.findAll();
    }
    
    public Optional<User> user(Long id) {
        return userRepository.findById(id);
    }
    
    public List<Post> posts() {
        return postRepository.findAll();
    }
    
    public Optional<Post> post(Long id) {
        return postRepository.findById(id);
    }
    
    // Field resolver for User.posts
    public List<Post> posts(User user) {
        return postRepository.findByAuthorId(user.getId());
    }
    
    // Field resolver for Post.author
    public User author(Post post) {
        return userRepository.findById(post.getAuthor().getId())
            .orElseThrow(() -> new RuntimeException("Author not found"));
    }
}

2. Mutation Resolvers:

@Component
public class UserMutationResolver implements GraphQLMutationResolver {
    
    private final UserRepository userRepository;
    private final PostRepository postRepository;
    
    public UserMutationResolver(UserRepository userRepository, PostRepository postRepository) {
        this.userRepository = userRepository;
        this.postRepository = postRepository;
    }
    
    public User createUser(String name, String email) {
        if (userRepository.existsByEmail(email)) {
            throw new RuntimeException("User with email " + email + " already exists");
        }
        
        User user = new User(name, email);
        return userRepository.save(user);
    }
    
    public User updateUser(Long id, String name, String email) {
        User user = userRepository.findById(id)
            .orElseThrow(() -> new RuntimeException("User not found"));
        
        if (name != null) {
            user.setName(name);
        }
        if (email != null) {
            user.setEmail(email);
        }
        
        return userRepository.save(user);
    }
    
    public Boolean deleteUser(Long id) {
        if (!userRepository.existsById(id)) {
            throw new RuntimeException("User not found");
        }
        
        userRepository.deleteById(id);
        return true;
    }
    
    public Post createPost(String title, String content, Long authorId) {
        User author = userRepository.findById(authorId)
            .orElseThrow(() -> new RuntimeException("Author not found"));
        
        Post post = new Post();
        post.setTitle(title);
        post.setContent(content);
        post.setAuthor(author);
        
        return postRepository.save(post);
    }
}

Advanced GraphQL Features

1. Input Types for Complex Mutations:

# Schema definition
input CreateUserInput {
  name: String!
  email: String!
  password: String!
}

input UpdateUserInput {
  name: String
  email: String
}

type Mutation {
  createUser(input: CreateUserInput!): User!
  updateUser(id: ID!, input: UpdateUserInput!): User!
}

Java Implementation:

// Input DTOs
public class CreateUserInput {
    private String name;
    private String email;
    private String password;
    
    // getters and setters
}

public class UpdateUserInput {
    private String name;
    private String email;
    
    // getters and setters
}

// Updated mutation resolver
public User createUser(CreateUserInput input) {
    User user = new User(input.getName(), input.getEmail());
    // Set password, etc.
    return userRepository.save(user);
}

public User updateUser(Long id, UpdateUserInput input) {
    User user = userRepository.findById(id)
        .orElseThrow(() -> new RuntimeException("User not found"));
    
    if (input.getName() != null) {
        user.setName(input.getName());
    }
    if (input.getEmail() != null) {
        user.setEmail(input.getEmail());
    }
    
    return userRepository.save(user);
}

2. Pagination with Connections:

# Schema with pagination
type UserConnection {
  edges: [UserEdge!]!
  pageInfo: PageInfo!
}

type UserEdge {
  node: User!
  cursor: String!
}

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

type Query {
  users(first: Int, after: String): UserConnection!
}

Java Implementation:

@Component
public class UserPaginationResolver {
    
    public UserConnection users(Integer first, String after) {
        // Parse cursor
        Long afterId = after != null ? Long.parseLong(after) : null;
        
        // Fetch users with pagination
        List<User> users = afterId != null ?
            userRepository.findByIdGreaterThan(afterId, PageRequest.of(0, first)) :
            userRepository.findAll(PageRequest.of(0, first));
        
        // Build connection
        List<UserEdge> edges = users.stream()
            .map(user -> new UserEdge(user, createCursor(user.getId())))
            .collect(Collectors.toList());
        
        boolean hasNextPage = users.size() == first;
        String endCursor = edges.isEmpty() ? null : edges.get(edges.size() - 1).getCursor();
        
        PageInfo pageInfo = new PageInfo(hasNextPage, false, edges.get(0).getCursor(), endCursor);
        
        return new UserConnection(edges, pageInfo);
    }
    
    private String createCursor(Long id) {
        return Base64.getEncoder().encodeToString(("user:" + id).getBytes());
    }
    
    // Connection classes
    public static class UserConnection {
        private final List<UserEdge> edges;
        private final PageInfo pageInfo;
        
        // constructor, getters
    }
    
    public static class UserEdge {
        private final User node;
        private final String cursor;
        
        // constructor, getters
    }
    
    public static class PageInfo {
        private final boolean hasNextPage;
        private final boolean hasPreviousPage;
        private final String startCursor;
        private final String endCursor;
        
        // constructor, getters
    }
}

GraphQL Subscriptions (Real-time)

WebSocket Configuration:

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(graphQLWebSocketHandler(), "/subscriptions")
                .setAllowedOrigins("*");
    }
    
    @Bean
    public WebSocketHandler graphQLWebSocketHandler() {
        return new GraphQLWebSocketHandler();
    }
}

Subscription Resolver:

@Component
public class PostSubscriptionResolver implements GraphQLSubscriptionResolver {
    
    private final Publisher<Post> postPublisher;
    private final PostService postService;
    
    public PostSubscriptionResolver(PostService postService) {
        this.postService = postService;
        this.postPublisher = postService.getPostPublisher();
    }
    
    public Publisher<Post> postAdded() {
        return postPublisher;
    }
    
    public Publisher<Post> postUpdated() {
        return postService.getPostUpdatedPublisher();
    }
}

Post Service with Event Publishing:

@Service
public class PostService {
    
    private final PostRepository postRepository;
    private final FluxSink<Post> postSink;
    private final Flux<Post> postFlux;
    
    public PostService(PostRepository postRepository) {
        this.postRepository = postRepository;
        this.postFlux = Flux.create(sink -> this.postSink = sink, FluxSink.OverflowStrategy.BUFFER);
    }
    
    public Post createPost(String title, String content, Long authorId) {
        Post post = new Post();
        post.setTitle(title);
        post.setContent(content);
        // set author, etc.
        
        Post savedPost = postRepository.save(post);
        
        // Publish to subscribers
        if (postSink != null) {
            postSink.next(savedPost);
        }
        
        return savedPost;
    }
    
    public Publisher<Post> getPostPublisher() {
        return postFlux;
    }
}

Error Handling in GraphQL

Custom GraphQL Errors:

@Component
public class CustomDataFetcherExceptionResolver implements DataFetcherExceptionResolver {
    
    @Override
    public List<GraphQLError> resolveException(Throwable exception, DataFetchingEnvironment environment) {
        if (exception instanceof UserNotFoundException) {
            return Arrays.asList(
                GraphqlErrorBuilder.newError()
                    .message(exception.getMessage())
                    .path(environment.getExecutionStepInfo().getPath())
                    .location(environment.getField().getSourceLocation())
                    .extensions(Map.of("code", "USER_NOT_FOUND", "userId", ((UserNotFoundException) exception).getUserId()))
                    .build()
            );
        } else if (exception instanceof ValidationException) {
            return Arrays.asList(
                GraphqlErrorBuilder.newError()
                    .message("Validation failed")
                    .path(environment.getExecutionStepInfo().getPath())
                    .location(environment.getField().getSourceLocation())
                    .extensions(Map.of("code", "VALIDATION_ERROR", "details", getValidationDetails(exception)))
                    .build()
            );
        }
        
        // Generic error
        return Arrays.asList(
            GraphqlErrorBuilder.newError()
                .message("Internal server error")
                .path(environment.getExecutionStepInfo().getPath())
                .location(environment.getField().getSourceLocation())
                .extensions(Map.of("code", "INTERNAL_ERROR"))
                .build()
        );
    }
    
    private Map<String, Object> getValidationDetails(Throwable exception) {
        // Extract validation details from exception
        return Map.of("message", exception.getMessage());
    }
}

// Custom exception
public class UserNotFoundException extends RuntimeException {
    private final Long userId;
    
    public UserNotFoundException(Long userId) {
        super("User with id " + userId + " not found");
        this.userId = userId;
    }
    
    public Long getUserId() {
        return userId;
    }
}

DataLoader for N+1 Problem

User DataLoader:

@Component
public class UserDataLoader {
    
    private final UserRepository userRepository;
    
    public UserDataLoader(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    @Bean
    public DataLoader<Long, User> userDataLoader() {
        return DataLoaderFactory.newDataLoader(this::loadUsers);
    }
    
    private CompletableFuture<List<User>> loadUsers(List<Long> userIds) {
        return CompletableFuture.supplyAsync(() -> 
            userRepository.findAllById(userIds)
        );
    }
}

// Using DataLoader in resolver
@Component
public class PostResolver implements GraphQLResolver<Post> {
    
    private final DataLoader<Long, User> userDataLoader;
    
    public CompletableFuture<User> author(Post post, DataFetchingEnvironment env) {
        return userDataLoader.load(post.getAuthor().getId());
    }
}

Security with GraphQL

GraphQL Security Configuration:

@Configuration
@EnableWebSecurity
public class GraphQLSecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/graphql").permitAll()
                .requestMatchers("/graphiql").permitAll()
                .anyRequest().authenticated()
            )
            .csrf(csrf -> csrf.ignoringRequestMatchers("/graphql"));
        
        return http.build();
    }
}

// Method-level security
@Component
public class SecureUserResolver implements GraphQLQueryResolver {
    
    @PreAuthorize("hasRole('ADMIN')")
    public List<User> allUsers() {
        return userRepository.findAll();
    }
    
    @PreAuthorize("isAuthenticated()")
    public User currentUser(Authentication authentication) {
        String username = authentication.getName();
        return userRepository.findByEmail(username)
            .orElseThrow(() -> new UserNotFoundException(username));
    }
}

Performance Monitoring and Metrics

GraphQL Metrics:

@Component
public class GraphQLMetrics {
    
    private final MeterRegistry meterRegistry;
    private final Counter queryCounter;
    private final Timer queryTimer;
    
    public GraphQLMetrics(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.queryCounter = Counter.builder("graphql.queries")
            .description("Number of GraphQL queries")
            .register(meterRegistry);
        this.queryTimer = Timer.builder("graphql.query.duration")
            .description("GraphQL query execution time")
            .register(meterRegistry);
    }
    
    @EventListener
    public void handleGraphQLStart(GraphQlStartEvent event) {
        queryCounter.increment();
    }
    
    @EventListener
    public void handleGraphQLEnd(GraphQlEndEvent event) {
        queryTimer.record(event.getDuration());
    }
}

Testing GraphQL APIs

Integration Tests:

@SpringBootTest
@AutoConfigureTestDatabase
class GraphQLIntegrationTest {
    
    @Autowired
    private TestGraphQlTemplate graphQlTemplate;
    
    @Test
    void shouldCreateAndRetrieveUser() {
        // Create user mutation
        String createUserMutation = """
            mutation {
                createUser(input: {
                    name: "John Doe",
                    email: "john@example.com",
                    password: "secret"
                }) {
                    id
                    name
                    email
                }
            }
            """;
        
        GraphQlResponse response = graphQlTemplate.post(createUserMutation);
        
        assertThat(response.isValid()).isTrue();
        assertThat(response.get("data.createUser.name")).isEqualTo("John Doe");
        
        // Query for user
        String getUserQuery = """
            query {
                user(id: 1) {
                    name
                    email
                }
            }
            """;
        
        GraphQlResponse queryResponse = graphQlTemplate.post(getUserQuery);
        assertThat(queryResponse.get("data.user.name")).isEqualTo("John Doe");
    }
}

Unit Tests:

@WebMvcTest
@Import({UserQueryResolver.class, UserMutationResolver.class})
class GraphQLControllerTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @MockBean
    private UserRepository userRepository;
    
    @Test
    void shouldExecuteUserQuery() throws Exception {
        User user = new User(1L, "John Doe", "john@example.com");
        when(userRepository.findAll()).thenReturn(Arrays.asList(user));
        
        String query = """
            {
                users {
                    id
                    name
                    email
                }
            }
            """;
        
        mockMvc.perform(post("/graphql")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"query\": \"" + query.replace("\"", "\\\"") + "\"}"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.data.users[0].name").value("John Doe"));
    }
}

Advanced Schema Design

1. Interface and Union Types:

interface SearchResult {
  id: ID!
  title: String!
}

type User implements SearchResult {
  id: ID!
  title: String!
  name: String!
  email: String!
}

type Post implements SearchResult {
  id: ID!
  title: String!
  content: String!
  author: User!
}

union SearchResultUnion = User | Post

type Query {
  search(query: String!): [SearchResult!]!
  searchUnion(query: String!): [SearchResultUnion!]!
}

Java Implementation:

@Component
public class SearchResolver implements GraphQLQueryResolver {
    
    public List<SearchResult> search(String query) {
        List<SearchResult> results = new ArrayList<>();
        
        // Search users
        results.addAll(userRepository.findByNameContaining(query));
        
        // Search posts
        results.addAll(postRepository.findByTitleContainingOrContentContaining(query, query));
        
        return results;
    }
}

2. Custom Scalars:

@Configuration
public class GraphQLScalarConfig {
    
    @Bean
    public GraphQLScalarType dateTimeScalar() {
        return ExtendedScalars.DateTime;
    }
    
    @Bean
    public GraphQLScalarType jsonScalar() {
        return ExtendedScalars.Json;
    }
}

Client-Side GraphQL Usage

JavaScript Client with Apollo:

import { ApolloClient, InMemoryCache, gql, useQuery, useMutation } from '@apollo/client';

const client = new ApolloClient({
  uri: 'http://localhost:8080/graphql',
  cache: new InMemoryCache()
});

// Query hook
const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
      email
      posts {
        title
        content
      }
    }
  }
`;

function UserList() {
  const { loading, error, data } = useQuery(GET_USERS);
  
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;
  
  return (
    <div>
      {data.users.map(user => (
        <div key={user.id}>
          <h3>{user.name}</h3>
          <p>{user.email}</p>
          <div>
            {user.posts.map(post => (
              <div key={post.id}>
                <h4>{post.title}</h4>
                <p>{post.content}</p>
              </div>
            ))}
          </div>
        </div>
      ))}
    </div>
  );
}

// Mutation hook
const CREATE_USER = gql`
  mutation CreateUser($input: CreateUserInput!) {
    createUser(input: $input) {
      id
      name
      email
    }
  }
`;

function CreateUserForm() {
  const [createUser, { data, loading, error }] = useMutation(CREATE_USER);
  
  const handleSubmit = (event) => {
    event.preventDefault();
    const formData = new FormData(event.target);
    
    createUser({
      variables: {
        input: {
          name: formData.get('name'),
          email: formData.get('email'),
          password: formData.get('password')
        }
      }
    });
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input name="name" placeholder="Name" required />
      <input name="email" type="email" placeholder="Email" required />
      <input name="password" type="password" placeholder="Password" required />
      <button type="submit" disabled={loading}>
        {loading ? 'Creating...' : 'Create User'}
      </button>
      {error && <p>Error: {error.message}</p>}
    </form>
  );
}

Best Practices for GraphQL APIs

1. Schema Design:

  • Use descriptive type and field names

  • Implement proper pagination

  • Use input types for mutations

  • Version through schema evolution, not breaking changes

2. Performance:

  • Use DataLoader to solve N+1 problems

  • Implement query complexity analysis

  • Cache frequently accessed data

  • Monitor and optimize resolver performance

3. Security:

  • Implement query depth limiting

  • Use query whitelisting in production

  • Validate and sanitize all inputs

  • Implement proper authentication and authorization

4. Error Handling:

  • Provide meaningful error messages

  • Use custom error types

  • Include error codes for client handling

  • Log errors appropriately

This extensive GraphQL tutorial covers everything from basic concepts to advanced implementation patterns, providing you with the knowledge to build efficient, flexible, and type-safe APIs that give clients exactly the data they need.

 

Comments (0)

No comments yet. Be the first to comment!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?