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!