Evolving your API without Versioning in GraphQL

Cover image for the blog post

If you've been building REST APIs for any length of time, you've probably wrestled with versioning. You know the drill: your perfectly designed /api/v1/users endpoint ships to production, clients integrate with it, and then... requirements change. Now you need to add fields, restructure responses, or worse, remove deprecated data. Welcome to versioning fun, where /api/v2/users is born, and your maintenance burden just doubled.

In my previous article on API versioning in Spring Boot 4, I covered the new features and strategies for implementing versioning in REST APIs. While Spring Boot 4 makes versioning easier with improved support for version negotiation and management, it doesn't eliminate the fundamental need for versioning in REST architectures.

But what if I told you there's a way to evolve your API without versioning? Enter GraphQL – where the whole concept of API versions becomes largely irrelevant. Let's explore why REST demands versioning and how GraphQL elegantly sidesteps this entire problem.

📦 Get the Code

Follow along with the complete working example.

github.com/danvega/gqlversion

The REST Versioning Dilemma

REST APIs return fixed response structures. When you hit /api/users/123, you get back a predetermined set of fields:

@RestController
@RequestMapping("/api/v1")
public class UserController {
    
    @GetMapping("/users/{id}")
    public UserResponse getUser(@PathVariable Long id) {
        return new UserResponse(
            id,
            "John Doe",           // full name as single field
            "[email protected]",
            LocalDateTime.now()
        );
    }
}

This works great until you need to split that name field into firstName and lastName. Now you're stuck. Change the response structure, and you break every client expecting the original format. The traditional solution? Create a new version:

@RestController
@RequestMapping("/api/v2")  // New version
public class UserV2Controller {
    
    @GetMapping("/users/{id}")
    public UserV2Response getUser(@PathVariable Long id) {
        return new UserV2Response(
            id,
            "John",              // Now split into
            "Doe",               // two fields
            "[email protected]",
            LocalDateTime.now()
        );
    }
}

In my article on Spring Boot 4 API versioning, I covered the various strategies for implementing versioning (URI versioning, request parameters, headers, etc.). While these approaches work, they all share the same fundamental problems that make REST API evolution painful.

The Hidden Costs of REST Versioning

Versioning isn't just about adding a "v2" to your URLs. It creates a cascade of complexity:

Maintenance Multiplication: Every version needs its own controllers, DTOs, documentation, and tests. Supporting three versions means triple the maintenance work.

// You end up with duplicate code everywhere
public class UserResponse { }      // v1
public class UserV2Response { }    // v2
public class UserV3Response { }    // v3

public class UserMapper { }        // v1
public class UserV2Mapper { }      // v2
public class UserV3Mapper { }      // v3

Database Evolution Complexity: Your database might support the latest structure, but you need mapping logic for older versions:

@Service
public class UserService {
    
    public Object getUser(Long id, String version) {
        User user = userRepository.findById(id);
        
        return switch(version) {
            case "v1" -> mapToV1(user);  // Combine firstName + lastName
            case "v2" -> mapToV2(user);  // Keep separate
            case "v3" -> mapToV3(user);  // Add new fields
            default -> mapToLatest(user);
        };
    }
}

Client Migration Fatigue: Every new version requires clients to update their integration, test the changes, and deploy updates. Many clients end up stuck on old versions, forcing you to maintain legacy code indefinitely.

GraphQL's Revolutionary Approach

GraphQL completely flips the script on API evolution. Instead of the server dictating the response structure, clients request exactly what they need:

# Client A wants minimal data
query {
  user(id: "123") {
    name
    email
  }
}

# Client B wants more detail
query {
  user(id: "123") {
    name
    email
    avatar
    posts {
      title
      publishedAt
    }
  }
}

Both queries hit the same endpoint, but each client gets precisely what they asked for. No versioning required.

Setting Up GraphQL in Spring Boot

Let's see how this works in practice. First, add Spring GraphQL to your project:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-graphql</artifactId>
</dependency>

Define your schema:

type User {
  id: ID!
  name: String @deprecated(reason: "Use firstName and lastName")
  firstName: String
  lastName: String
  email: String
  avatar: String
  createdAt: String
}

type Query {
  user(id: ID!): User
}

Implement the resolver:

@Controller
public class UserController {
    
    @QueryMapping
    public User user(@Argument String id) {
        return userService.findById(id);
    }
    
    @SchemaMapping(typeName = "User", field = "name")
    public String name(User user) {
        // Maintain backward compatibility
        return user.getFirstName() + " " + user.getLastName();
    }
}

The Magic of Field-Level Evolution

Here's where GraphQL shines. Want to add a new field? Just add it to the schema:

type User {
  id: ID!
  firstName: String
  lastName: String
  email: String
  avatar: String
  createdAt: String
  # New field added - existing clients unaffected
  lastLoginAt: String
}

Existing clients that don't request lastLoginAt continue working without any changes. New clients can start using it immediately. No new endpoints, no version numbers, no migration pain.

Handling Breaking Changes Gracefully

When you do need to make breaking changes, GraphQL's @deprecated directive provides a smooth transition path:

@Component
public class UserResolver {
    
    // Old field (deprecated)
    @SchemaMapping(typeName = "User", field = "name")
    public String name(User user) {
        log.warn("Client still using deprecated 'name' field");
        return user.getFirstName() + " " + user.getLastName();
    }
}

Tools like GraphiQL and Apollo Studio will show deprecation warnings, guiding developers to update their queries. You can track deprecated field usage and safely remove them once all clients have migrated.

Making the Choice

The decision between REST with versioning and GraphQL without versioning comes down to your specific needs:

Choose REST with versioning when:

  • Your API changes infrequently
  • You need simple HTTP caching
  • Your team lacks GraphQL expertise
  • You're building a simple, resource-oriented API

Choose GraphQL when:

  • Your API evolves frequently
  • You have diverse clients with different data needs
  • You want to minimize client-server coordination
  • You're building a data-rich, complex API

Conclusion

API versioning in REST is a necessary evil that creates maintenance headaches, client migration challenges, and technical debt. GraphQL's client-driven queries and field-level deprecation offer an elegant alternative that lets your API evolve naturally without versioning.

For Spring Boot developers, adopting GraphQL doesn't mean abandoning everything you know. Spring GraphQL builds on familiar Spring concepts while providing the flexibility to evolve your API without the versioning burden. The next time you're about to create /api/v2, consider whether GraphQL might save you from versioning altogether.

By the way this is just one of the features I love about GraphQL, there are many others. Have you made the switch from REST to GraphQL? What was your experience with API evolution? I'd love to hear about your journey on social media.

Happy coding, friends! 🚀

Subscribe to my newsletter.

Sign up for my weekly newsletter and stay up to date with current blog posts.

Weekly Updates
I will send you an update each week to keep you filled in on what I have been up to.
No spam
You will not receive spam from me and I will not share your email address with anyone.