GraphQL with Java

Build Flexible APIs - Let Clients Ask for Exactly What They Need

Why GraphQL?

REST APIs have a problem: you get what the server decides, not what you need. Want just the user's name? Too bad, here's their entire profile. Need data from 3 endpoints? Make 3 requests.

GraphQL flips this: clients request exactly what they need in a single query. No over-fetching, no under-fetching.

// REST: Multiple requests, over-fetching
GET /users/1        → { id, name, email, address, phone, ... }
GET /users/1/posts  → [ { id, title, content, ... }, ... ]
GET /users/1/friends → [ ... ]

// GraphQL: One request, exact data
query {
  user(id: 1) {
    name
    posts {
      title
    }
    friends {
      name
    }
  }
}

Setup with Spring Boot

<!-- pom.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>
# application.properties
spring.graphql.graphiql.enabled=true
spring.graphql.path=/graphql

Define Your Schema

Create src/main/resources/graphql/schema.graphqls:

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

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

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

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

input CreateUserInput {
    name: String!
    email: String!
}

input UpdateUserInput {
    name: String
    email: String
}

Implement Resolvers

@Controller
public class UserController {

    @Autowired
    private UserService userService;

    // Query resolver
    @QueryMapping
    public List<User> users() {
        return userService.findAll();
    }

    @QueryMapping
    public User user(@Argument Long id) {
        return userService.findById(id);
    }

    // Mutation resolver
    @MutationMapping
    public User createUser(@Argument CreateUserInput input) {
        return userService.create(input.getName(), input.getEmail());
    }

    @MutationMapping
    public User updateUser(@Argument Long id, @Argument UpdateUserInput input) {
        return userService.update(id, input);
    }

    @MutationMapping
    public boolean deleteUser(@Argument Long id) {
        return userService.delete(id);
    }

    // Field resolver - loads posts for a user
    @SchemaMapping(typeName = "User", field = "posts")
    public List<Post> posts(User user) {
        return postService.findByAuthorId(user.getId());
    }
}

Making Queries

# Get all users with their posts
query {
  users {
    id
    name
    posts {
      title
    }
  }
}

# Get specific user
query {
  user(id: "1") {
    name
    email
  }
}

# Create a user
mutation {
  createUser(input: { name: "John", email: "john@email.com" }) {
    id
    name
  }
}

# Variables (better for dynamic data)
mutation CreateUser($input: CreateUserInput!) {
  createUser(input: $input) {
    id
    name
  }
}

# With variables:
# { "input": { "name": "John", "email": "john@email.com" } }

N+1 Problem & DataLoader

Without optimization, fetching users with posts makes N+1 database calls. DataLoader batches these.

@Configuration
public class DataLoaderConfig {

    @Bean
    public BatchLoaderRegistry batchLoaderRegistry(PostService postService) {
        return registry -> {
            registry.forTypePair(Long.class, List.class)
                .registerMappedBatchLoader((userIds, env) -> {
                    // Single query for all posts
                    Map<Long, List<Post>> postsByUser = postService.findByAuthorIds(userIds);
                    return Mono.just(postsByUser);
                });
        };
    }
}

Error Handling

@ControllerAdvice
public class GraphQLExceptionHandler {

    @ExceptionHandler(UserNotFoundException.class)
    public GraphQLError handleUserNotFound(UserNotFoundException ex) {
        return GraphQLError.newError()
            .errorType(ErrorType.NOT_FOUND)
            .message(ex.getMessage())
            .build();
    }
}

Master Modern API Development with GraphQL

Learn GraphQL, REST, and API design with hands-on projects.

Explore Full Stack Java Course