Building a REST API with Spring Boot and @RestController
Introduction
This tutorial builds a complete REST API with Spring Boot and@RestController: a small book catalogue that clients can list, read, create, update and delete over HTTP. We will start with what a REST API is - resources, URLs, HTTP methods and JSON - and how Spring MVC handles a request. We will then write the controller one endpoint at a time and call every endpoint with real HTTP requests, looking at the status codes and JSON that come back. Next we will see how Spring turns Java objects into JSON and back, and what @RestController really is. Lastly, we will handle a missing book with a 404 response and list the design rules that make an API pleasant to use.(This tutorial starts module 3 of the Spring series, on REST APIs. It assumes the basics from Your First Spring Boot Application with Spring InitializrClick to read tutorial: Your First Spring Boot Application with Spring Initializr; the examples use the same
spring-boot-starter-webmvc dependency.)
What is a REST API?
REST (Representational State Transfer) is a style of designing web services around resources - the things an application manages, such as books, orders or customers. A REST API exposes each resource at a URL and lets clients act on it with the standard HTTP methods -GET /api/books- read the collection of books.GET /api/books/2- read the book with id 2.POST /api/books- create a new book from the data in the request body.PUT /api/books/2- replace book 2 with the data in the request body.DELETE /api/books/2- delete book 2.
200 OK, 201 Created, 204 No Content, 404 Not Found and so on. Each request carries everything the server needs; the server keeps no per-client session between requests. This is what makes REST APIs easy to call from any language, and easy to scale across several servers.
How Spring MVC handles a request
Spring MVC is built around one servlet, theDispatcherServlet, which Spring Boot registers in the embedded Tomcat. For every request it -
- finds the handler method whose mapping matches the request's HTTP method and path;
- converts the request's path variables, query parameters and JSON body into the method's arguments;
- calls the method;
- converts the return value into the response body - for a REST controller, JSON - using an HTTP message converter, and sets the status code.
The book catalogue API
The application has three parts: aBook record, a small in-memory repository (a database comes in module 4), and the controller. All of the code is in one file to keep the example easy to follow -
BookApiApp.java - a complete REST API for books
//BookApiApp.java
package com.javabrahman.rest.books;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Repository;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
record Book(Long id, String title, String author, int year) {
Book withId(Long newId) {
return new Book(newId, title, author, year);
}
}
@Repository
class BookRepository {
private final Map<Long, Book> books = new ConcurrentHashMap<>();
private final AtomicLong nextId = new AtomicLong();
BookRepository() {
save(new Book(null, "Effective Java", "Joshua Bloch", 2018));
save(new Book(null, "Java Concurrency in Practice", "Brian Goetz", 2006));
}
List<Book> findAll() {
return books.values().stream().sorted((a, b) -> a.id().compareTo(b.id())).toList();
}
Optional<Book> findById(long id) {
return Optional.ofNullable(books.get(id));
}
Book save(Book book) {
Book stored = book.id() == null ? book.withId(nextId.incrementAndGet()) : book;
books.put(stored.id(), stored);
return stored;
}
boolean delete(long id) {
return books.remove(id) != null;
}
}
@RestController
@RequestMapping("/api/books") // common path prefix for every method below
class BookController {
private final BookRepository repository;
BookController(BookRepository repository) {
this.repository = repository;
}
@GetMapping
List<Book> all() {
return repository.findAll();
}
@GetMapping("/{id}")
Book one(@PathVariable long id) {
return repository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "No book with id " + id));
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
Book create(@RequestBody Book book) {
return repository.save(book.withId(null)); // the server, not the client, chooses the id
}
@PutMapping("/{id}")
Book replace(@PathVariable long id, @RequestBody Book book) {
one(id); // 404 if it does not exist
return repository.save(book.withId(id));
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
void delete(@PathVariable long id) {
if (!repository.delete(id)) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "No book with id " + id);
}
}
}
@SpringBootApplication
public class BookApiApp {
public static void main(String[] args) {
SpringApplication.run(BookApiApp.class, args);
}
}
Explanation of the code
@RestControllermarksBookControlleras a bean whose methods handle web requests and return response bodies.@RequestMapping("/api/books")on the class sets the path prefix; each method's mapping adds to it, so@GetMapping("/{id}")handlesGET /api/books/{id}.@GetMapping,@PostMapping,@PutMappingand@DeleteMappingchoose the HTTP method. They are shortcuts for@RequestMapping(method = ...).@PathVariable long idtakes the{id}part of the path and converts it to along.@RequestBody Book bookconverts the JSON request body into aBook. Both are covered in detail in Spring MVC Request MappingClick to read tutorial: Spring MVC Request Mapping: Paths, Params and Bodies.- Methods return plain Java objects - a
List<Book>, aBook- and Spring writes them as JSON. By default the status is200 OK;@ResponseStatuschanges it to201 Createdfor the POST and204 No Contentfor the DELETE. ResponseStatusExceptionends the request with an error status and a reason; Spring Boot turns it into an error response.- The repository uses a
ConcurrentHashMapand anAtomicLongbecause the controller is a singletonClick to read tutorial: Spring Bean Scopes: Singleton, Prototype, Request, Session serving many requests on many threads at once.
Calling the API
With the application running, these requests exercise every endpoint in turn - list, read, create, read the new book, replace it, delete one, and finally list again. The responses are real, from the running application; each one shows the status line, the headers and the body, ascurl -i prints them -
OUTPUT of the HTTP requests> curl -i http://localhost:8080/api/books
HTTP/1.1 200
Content-Length: 154
Content-Type: application/json
[{"id":1,"title":"Effective Java","author":"Joshua Bloch","year":2018},{"id":2,"title":"Java Concurrency in Practice","author":"Brian Goetz","year":2006}]
> curl -i http://localhost:8080/api/books/1
HTTP/1.1 200
Content-Length: 69
Content-Type: application/json
{"id":1,"title":"Effective Java","author":"Joshua Bloch","year":2018}
> curl -i -X POST http://localhost:8080/api/books -H "Content-Type: application/json" -d '{"title":"Modern Java in Action","author":"Raoul-Gabriel Urma","year":2018}'
HTTP/1.1 201
Content-Length: 82
Content-Type: application/json
{"id":3,"title":"Modern Java in Action","author":"Raoul-Gabriel Urma","year":2018}
> curl -i http://localhost:8080/api/books/3
HTTP/1.1 200
Content-Length: 82
Content-Type: application/json
{"id":3,"title":"Modern Java in Action","author":"Raoul-Gabriel Urma","year":2018}
> curl -i -X PUT http://localhost:8080/api/books/3 -H "Content-Type: application/json" -d '{"title":"Modern Java in Action","author":"Raoul-Gabriel Urma, Mario Fusco, Alan Mycroft","year":2019}'
HTTP/1.1 200
Content-Length: 109
Content-Type: application/json
{"id":3,"title":"Modern Java in Action","author":"Raoul-Gabriel Urma, Mario Fusco, Alan Mycroft","year":2019}
> curl -i -X DELETE http://localhost:8080/api/books/2
HTTP/1.1 204
> curl -i http://localhost:8080/api/books
HTTP/1.1 200
Content-Length: 181
Content-Type: application/json
[{"id":1,"title":"Effective Java","author":"Joshua Bloch","year":2018},{"id":3,"title":"Modern Java in Action","author":"Raoul-Gabriel Urma, Mario Fusco, Alan Mycroft","year":2019}]
Explanation of the responses
- The first
GETreturned the two books the repository starts with, as a JSON array, withContent-Type: application/json. - The
POSTsent a book without an id; the API stored it as book 3 and answered201with the stored book, id included. - The
PUTreplaced the whole of book 3 with the new author list and year. - The
DELETEanswered204with no body - there is nothing to return about a deleted resource. - The final
GETshows the result of all the changes: book 2 is gone and book 3 has its new data.
From Java objects to JSON and back
Spring MVC does not know JSON itself. It delegates to HTTP message converters, and Spring Boot configures one for JSON backed by Jackson - in Spring Boot 4, Jackson 3. When a method returns aBook, the converter writes its record components as JSON fields; when a method takes a @RequestBody Book, the converter reads the JSON fields into the record's constructor. Records, classes with getters, lists, maps, dates and enums all work out of the box. Which converter is used depends on the request's Content-Type and Accept headers, which is the subject of content negotiation in CORS, Content Negotiation and File UploadsClick to read tutorial: CORS, Content Negotiation and File Uploads in Spring Boot.
What @RestController is
@RestController is a combination of two annotations -
@Controller, which makes the class a component that Spring MVC searches for handler methods;@ResponseBody, which tells Spring MVC to write each method's return value straight into the response body, instead of treating it as the name of a view template.
@Controller is used for server-rendered HTML pages with a template engine such as Thymeleaf, where a method returns a view name. For APIs, @RestController is the right choice.
When a book does not exist
Asking for a book that is not there, or deleting it, should answer404 Not Found rather than 200 with an empty body or a 500 server error. The ResponseStatusException in the controller does exactly that -
OUTPUT of requests for a book that does not exist> curl -i http://localhost:8080/api/books/99
HTTP/1.1 404
Content-Type: application/json
{"timestamp":"2026-11-06T09:17:30.776Z","status":404,"error":"Not Found","path":"/api/books/99"}
> curl -i -X DELETE http://localhost:8080/api/books/99
HTTP/1.1 404
Content-Type: application/json
{"timestamp":"2026-11-06T09:17:30.882Z","status":404,"error":"Not Found","path":"/api/books/99"}
REST API design rules of thumb
- Name resources with plural nouns -
/api/books,/api/books/3- and let the HTTP method say what happens. Avoid verbs in paths such as/api/getBook. - Use the right method:
GETnever changes data;PUTandDELETEgive the same result however many times they are sent (they are idempotent);POSTcreates. - Return meaningful status codes: 201 for a created resource, 204 when there is no body, 400 for invalid input, 404 for a missing resource. ResponseEntity and HTTP status codesClick to read tutorial: ResponseEntity and HTTP Status Codes in Spring Boot goes further, including the
Locationheader for a created resource. - Let the server assign ids, as the
create()method does, and validate input, covered in Bean Validation in Spring BootClick to read tutorial: Bean Validation in Spring Boot: @Valid and Custom Validators. - Keep the API stable: once clients depend on a field or a URL, changing it breaks them. API versioningClick to read tutorial: API Versioning in Spring Framework 7 and Spring Boot 4 shows how to evolve an API safely.
Summary
In this tutorial we first saw what a REST API is - resources at URLs, HTTP methods, JSON and status codes - and how theDispatcherServlet routes a request to a handler method. We then wrote a complete book catalogue API with @RestController and the @GetMapping, @PostMapping, @PutMapping and @DeleteMapping annotations, and called every endpoint with real requests, getting 200, 201 and 204 responses. Next we saw how Jackson-based message converters turn objects into JSON and back, and what @RestController combines. Lastly, we answered 404 for missing books with ResponseStatusException and listed REST design rules of thumb. The next tutorial looks closely at how requests reach a method: request mapping, path variables, parameters and bodiesClick to read tutorial: Spring MVC Request Mapping: Paths, Params and Bodies.Spring REST API Tutorials
Spring MVC Request Mapping: Paths, Params and BodiesClick to read tutorial: Spring MVC Request Mapping: Paths, Params and Bodies ResponseEntity and HTTP Status Codes in Spring BootClick to read tutorial: ResponseEntity and HTTP Status Codes in Spring Boot Bean Validation in Spring Boot: @Valid and Custom ValidatorsClick to read tutorial: Bean Validation in Spring Boot: @Valid and Custom Validators REST Error Handling with @ControllerAdvice and ProblemDetailClick to read tutorial: REST Error Handling with @ControllerAdvice and ProblemDetail API Versioning in Spring Framework 7 and Spring Boot 4Click to read tutorial: API Versioning in Spring Framework 7 and Spring Boot 4 Pagination, Sorting and Filtering in Spring Boot REST APIsClick to read tutorial: Pagination, Sorting and Filtering in Spring Boot REST APIs Calling REST Services with RestClient and HTTP InterfacesClick to read tutorial: Calling REST Services with RestClient and HTTP Interfaces
Spring MVC Request Mapping: Paths, Params and BodiesClick to read tutorial: Spring MVC Request Mapping: Paths, Params and Bodies ResponseEntity and HTTP Status Codes in Spring BootClick to read tutorial: ResponseEntity and HTTP Status Codes in Spring Boot Bean Validation in Spring Boot: @Valid and Custom ValidatorsClick to read tutorial: Bean Validation in Spring Boot: @Valid and Custom Validators REST Error Handling with @ControllerAdvice and ProblemDetailClick to read tutorial: REST Error Handling with @ControllerAdvice and ProblemDetail API Versioning in Spring Framework 7 and Spring Boot 4Click to read tutorial: API Versioning in Spring Framework 7 and Spring Boot 4 Pagination, Sorting and Filtering in Spring Boot REST APIsClick to read tutorial: Pagination, Sorting and Filtering in Spring Boot REST APIs Calling REST Services with RestClient and HTTP InterfacesClick to read tutorial: Calling REST Services with RestClient and HTTP Interfaces