Producing a Prototype REST API with Spring
Learn why you would use REST, how it differs from other methods, and how to set up your own basic REST server application and test it.
May 21, 2024 • 7 Minute Read
In this article, we’ll cover how to get started with REST in Spring. We’ll touch on some of the motivations for choosing REST, and how to get a prototype quickly up and going so you can learn and demonstrate the benefits it may hold for your use cases.
Spring tutorial series
- How to get started with the Spring Framework
- What is Spring AI, and how to use it: A beginner's guide
- How to use GraphQL in Spring: An easy step-by-step tutorial
- Spring Security: How to get started
- How to optimize Java startup and runtime performance (Using Spring's PetClinic)
Why use REST?
In 2000, Roy Fielding published his dissertation, outlining the principles of REST. Its primary attraction at the time was the idea that HTTP itself had sufficient concepts and abstractions to build an API on top of it. This appeared to be an improvement over one of its contemporaries, SOAP, which was protocol-agnostic, but also required clients to then speak that specialized language. If applications built their APIs using HTTP concepts like methods and status codes, it would be much easier to develop, test, and ultimately adopt.
History proved this to be largely correct for many years. Today, countless REST APIs interoperate with one another. Entire organizations publish dozens, hundreds, and even thousands of microservices, each using REST as their underlying model for designing APIs using HTTP. REST allows for thinking about our data in simple, stateless ways and today is the de-facto standard.
Even though REST is ubiquitous, there are other alternatives with other strengths. Read my article "How to use GraphQL in Spring" to learn about when you might want to use GraphQL over REST.
First, start with SpringInitializr
When you are building a Spring application, https://start.spring.io is a great place to begin. If you are following along, pick the latest Spring Boot and Java versions, and then select the Spring Web module like in this screenshot:
Then, download the artifact and import into your favorite IDE.
Aside from building a skeleton application, it added the following dependency:
implementation 'org.springframework.boot:spring-boot-starter-web'
...which will give us the needed Spring support for building a REST-enabled service.
Modeling your resources
Let’s introduce a simple Java class called Train:
public class Train {
private String id;
private String conductor;
private Integer capacity;
public Train() {}
public Train(String conductor, Integer capacity) {
this.id = UUID.randomUUID().toString();
this.conductor = conductor;
this.capacity = capacity;
}
// … getters and setters for each field
}
You may recognize this as a traditional Java POJO. A Java POJO is a good fit here as it will be easier to integrate later with something like Spring JPA. We can easily add database relationships like the train’s routes or other entities.
In this article, though, we’ll simply focus on the REST concepts and leave out how to tie this to a database for later.
Defining operations
When defining a REST API, we usually think about each resource in four operations: create, retrieve, update, and delete. These are mapped to four HTTP methods: POST, GET, PUT, and DELETE, respectively.
In Spring, you can annotate Java methods with @GetMapping, @PostMapping, @PutMapping, and @DeleteMapping to perform the four different operations.
Let’s define that controller now with stub methods for each:
@RestController
@RequestMapping("/trains")
public class TrainController {
@GetMapping
public Collection<Train> findTrains() {
// …
}
@GetMapping(“/{id}”)
public ResponseEntity<Train> findTrain(@PathVariable(“id”) String id) {
// …
}
@PostMapping
public ResponseEntity<Train> createTrain(@RequestBody Train body) {
// …
}
@PutMapping(“/{id}”)
public ResponseEntity<Train> updateTrain(@RequestBody Train body, @PathVariable(“id”) String id) {
// …
}
@DeleteMapping(“/{id}”)
public ResponseEntity<?> deleteTrain(@PathVariable(“id”) String id) {
// …
}
}
Using this configuration, Spring will list for REST requests like GET /trains/123, DELETE /trains/123, and POST /trains, allowing applications to interact with the train resources using HTTP primitives. The ResponseEntity return type is so that we can return a RESTful status code along with the domain object.
To add some data to return, let’s create a map like so:
@RestController
@RequestMapping("/trains")
public class TrainController {
final Map<String, Train> trains = List.of(
new Train(“James”, 50), new Train(“Carla”, 75), new Train(“Peabody”, 20))
.stream().collect(Collectors.toMap(Train::getId, Function.identity());
// …
}
GET
And finally, we can implement each method. findTrains and findTrain look like this:
@GetMapping
public Collection<Train> findTrains() {
return this.trains.values();
}
@GetMapping(“/{id}”)
public ResponseEntity<Train> findTrain(@PathVariable(“id”) String id) {
return Optional.ofNullable(this.trains.get(id))
.map(ResponseEntity::ok)
.orElseGet(() -> ResponseEntity.notFound().build());
}
Notice in findTrain, we make sure to handle returning a 404 if we couldn’t find the requested train, as per REST norms.
POST
Spring Boot automatically configures your application to map JSON to Java objects. Given that, we can take the values from the request and add a new train to our map like so:
@PostMapping
public ResponseEntity<Train> createTrain(@RequestBody Train body) {
Train train = new Train(body.getConductor(), body.getCapacity());
this.trains.put(train.getId(), train);
return ResponseEntity.created(URI.create("/trains/" + train.getId())).body(train);
}
Notice that we don’t simply add the train that the client sends as we don’t want to trust the client with the capacity to determine the primary key of the record. Also, we use ResponseEntity#created to return a 201 status code, the RESTful code for when an entity is created.
PUT
Then, updating looks a little different. We need to retrieve the existing value and update its properties like so:
@PutMapping(“/{id}”)
public ResponseEntity<Train> updateTrain(@RequestBody Train body, @PathVariable(“id”) String id) {
Train train = this.trains.get(id);
if (train == null) {
return ResponseEntity.notFound.build();
}
train.setConductor(body.getConductor());
train.setCapacity(body.getCapacity());
return ResponseEntity.ok(train);
}
DELETE
And finally, we can add deleting by using another built-in map method:
@DeleteMapping(“/{id}”)
public ResponseEntity<?> deleteTrain(@PathVariable(“id”) String id) {
this.trains.remove(id);
return ResponseEntity.noContent().build();
}
In this last method, we use ResponseEntity::noContent to return a 204, the RESTful status code for deletion.
Running a few queries
Great! Now we’re ready to try out a couple of queries.
Start the server by running the main method in TrainRestApplication. Then use HTTPie to set HTTP commands to the REST API.
Here is what GET /trains may look like:
http :8080/trains
[
{
"capacity": 50,
"conductor": "James",
"id": "d95c1531-5e85-43f3-b7d4-43c400880944"
},
{
"capacity": 20,
"conductor": "Peabody",
"id": "fe76b1bb-4c1d-47ea-8876-92a318f5df38"
},
{
"capacity": 75,
"conductor": "Carla",
"id": "6a5bb358-e30a-420b-b17d-f971fdfd95fd"
}
]
And now, if I want to change the capacity of the first train, say when someone boards, I can do an HTTP PUT like so:
http PUT :8080/trains/6a5bb358-e30a-420b-b17d-f971fdfd95fd conductor=Carla capacity=74
And see the following response:
{
"capacity": 74,
"conductor": "Carla",
"id": "6a5bb358-e30a-420b-b17d-f971fdfd95fd"
}
Testing REST Endpoints
Let’s end by adding some tests to make sure our endpoints continue to work as expected.
Create a new test file in src/test/java called TrainControllerTests and place the following inside:
@WebMvcTest(TrainController.class)
public class TrainControllerTests {
@Autowired
MockMvc mvc;
@Autowired
TrainController controller;
@Test
public void shouldGetTrains() throws Exception {
this.mvc.perform(get("/trains"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(3));
}
@Test
public void shouldGetTrain() throws Exception {
Train expected = this.controller.trains.values().iterator().next();
this.mvc.perform(get("/trains/" + expected.getId()))
.andExpect(jsonPath("$.id").value(expected.getId()));
}
@Test
public void postShouldCreateTrain() throws Exception {
this.mvc.perform(post("/trains")
.contentType("application/json")
.content("""
{
"conductor": "Georgia",
"capacity": 48
}
"""))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.conductor").value("Georgia"));
}
@Test
public void putShouldUpdateTrain() throws Exception {
Train expected = this.controller.trains.values().iterator().next();
this.mvc.perform(put("/trains/" + expected.getId())
.contentType("application/json")
.content("""
{
"conductor": "Jason",
"capacity": 47
}
"""))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(expected.getId()))
.andExpect(jsonPath("$.capacity").value(47));
}
@Test
public void deleteShouldhouldRemoveTrain() throws Exception {
int size = this.controller.trains.size();
Train removed = this.controller.trains.values().iterator().next();
this.mvc.perform(delete("/trains/" + removed.getId()))
.andExpect(status().isNoContent());
Assertions.assertThat(this.controller.trains).hasSize(size - 1);
}
}
Each of these tests uses Spring Framework’s MockMvc API to send mock requests for each HTTP method. Note that each one verifies that the prescribed change happens as well as having the expected RESTful status code.
Conclusion
In this article, you learned some of the motivations for REST and how it differs from other ways to architect your API.
Then, you learned how to set up the skeleton of a basic REST server application. We added the appropriate domain object and controller methods to accept the four main HTTP operations. And we also added testing to ensure that it continues to work down the road.
Now, think about your use cases and try building something yourself!
Learning more about Java and Spring
If you’re keen to upgrade your Java skills, Pluralsight also offers a wide range of Java and Spring related courses that cater to your skill level, where you can sign up for a 10-day free trial. You can also perform a free roleIQ assessment to see where your Java skills currently stack up, with advice on where to shore up the gaps in your current skill set.
Below are three Pluralsight learning paths with beginner, intermediate, and advanced Java courses — just pick the starting point that works for you. If you’re not sure where you’re at, each page has a skill assessment you can use that will recommend where you should start out.
- Java Coding Practices
- Java SE 11 Developer Certification (1ZO-819)
- Spring Framework 6 and Spring Boot 3