Overloading REST controller endpoints on Spring Boot

Sometimes the requirements are bad, other times the tech debt is what limits you.

One particular situation I have come across is the need for the same endpoint and method but with different parameters and different behaviours e.g.

http://example.com/test?foo=bobby
http://example.com/test?bar=tables

Now the standard way to handle this would be to make one endpoint with two optional parameters like so in a rest controller.

@GetMapping("/endpoint")
public Response getStuff(@RequestParam(value = "foo", required = false) String foo, 
			@RequestParam(value = "bar", required = false) String bar) {
		
		if (foo != null)
			// do stuff for foo
		else if (bar != null)
			// do stuff for bar
}

However if you do not have the luxury of a thin controller layer, or have other limitations preventing you from doing so, there is another way.

Spring boot at runtime registers each endpoint with a unique signature, you can see this in the startup logs e.g.

Mapped “{[/endpoint],methods=[GET],produces=[application/json]}" onto public org.springframework.http.Response<> com.example.controller.ExampleController.getStuff(java.lang.String, java.lang.String)

This signature is based on the URI, HTTP method, produces, consumes, parameters and the REST controller function it invokes.

Yo could try some good old Java functional overloading like so.

@GetMapping("/endpoint")
public Response getStuff(@RequestParam(value = "foo") String foo) {
		
		// do stuff for foo
}

@GetMapping("/endpoint")
public Response getStuff(@RequestParam(value = "bar") String bar) {
		
		// do stuff for bar
}

However it will cause Spring boot to throw an Ambiguous endpoint exception and it will prevent the service from starting.

Caused by: java.lang.IllegalStateException: Ambiguous mapping found.

To make this workaround complete you need to alter how Spring calculates the endpoint's signature when it tries to map it. In this example we can use the @GetMapping annotation to explicitly specify what parameters each endpoint is expected to have. The same can be done for other attributes on the function.

@GetMapping(value = "/endpoint", params = { "foo" })
public Response getStuff(@RequestParam(value = "foo") String foo) {

        // do stuff for foo
}

@GetMapping("/endpoint", params = { "bar" })
public Response getStuff(@RequestParam(value = "bar") String bar) {

        // do stuff for bar
}

This feature, allows great flexibility but it comes with a risk in the form of below:

www.example.com/endpoint?foo=bobby&bar=tables

This will cause Spring boot to blow up, as it is unable to resolve to the appropriate function in the REST controller.

To solve this, one of the endpoints can be used as the default. The additional parameter will be ignored.

@GetMapping(value = "/endpoint", params = { "foo", "bar" })
public Response getStuff(@RequestParam(value = "foo") String foo
@RequestParam(value = "bar", required = false)) String bar {

        // do stuff for foo by default, ignore bar variable
}

@GetMapping("/endpoint", params = { "bar" })
public Response getStuff(@RequestParam(value = "bar") String bar) {

        // do stuff for bar
}

This is clearly an anti-pattern in terms of RESTful design, however sometimes you have to work within the limitations of an architecture and this will hopefully provide you with a solution.