Spring Boot Course · Lesson 2
In Lesson 1 you returned a plain String. Real APIs return data — structured objects, as JSON. Here you'll model your first domain type, the Book, and watch Spring turn Java objects and lists into JSON automatically.
GET /api/books returns a JSON array of books and GET /api/books/featured returns a single book object — your first real API resource.
Recall the key idea from last time: @RestController = @Controller + @ResponseBody. The @ResponseBody part means "whatever this method returns IS the HTTP response body" — not the name of an HTML page to render. When you returned a String, it went back as plain text.
Return an object instead, and Spring does something smarter: it serialises it to JSON. JSON (JavaScript Object Notation) is the lingua franca of REST APIs — a compact, language-neutral text format that every client, from a browser to a mobile app, can read. Our job in this lesson is just to hand Spring the right Java objects; it handles the conversion.
A REST API is organised around resources — the nouns your API exposes. Ours is the Book. Before we write it, a word on where code lives.
Book record, its controller, and (later) its service and repository all go in com.example.library.book; authors get their own com.example.library.author package in a later lesson. The alternative — grouping by technical layer (a controller package, a service package, …) — is common in tutorials, but package-by-feature keeps everything you touch for one change side by side, and it scales better as the API grows.
One rule makes this work: @SpringBootApplication scans its own package and every package below it. Because book sits under com.example.library (where LibraryApplication lives), your classes are discovered automatically. Put code in a sibling or parent package and component scanning silently misses it.
Now the record itself. Because you know Java well, we'll use a record — a perfect fit for a simple, immutable data carrier:
src/main/java/com/example/library/book/Book.java
package com.example.library.book;
import java.util.UUID;
public record Book(UUID id, String title, String author, int year) {
}
That single line gives you a constructor, accessors (id(), title(), …), plus equals/hashCode/toString. Jackson reads those accessors to discover the fields it should turn into JSON — so a record works out of the box. (A plain class with getters works identically; the record is just less typing.)
UUID for the id?
The most common default in Spring tutorials is a numeric Long that the database auto-increments. We're using a UUID instead: a 128-bit globally-unique identifier. It can be generated by the app (no round-trip to the database to learn the id), it's not sequentially guessable (so clients can't enumerate your books by counting 1, 2, 3…), and it stays unique even if you later merge data from multiple sources. The cost is a larger, less human-friendly id — a trade we're happy to make here. Jackson serialises a UUID as a JSON string, which you'll see in a moment. When we reach persistence in Lesson 5, the database will generate these for us.
Now a controller that hands back one Book. Create:
src/main/java/com/example/library/book/BookController.java
package com.example.library.book;
import java.util.UUID;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class BookController {
@GetMapping("/api/books/featured")
public Book featured() {
return new Book(UUID.fromString("11111111-1111-1111-1111-111111111111"),
"Clean Code", "Robert C. Martin", 2008);
}
} Run the app (./mvnw spring-boot:run) and open http://localhost:8080/api/books/featured. You'll see:
{ "id": "11111111-1111-1111-1111-111111111111", "title": "Clean Code", "author": "Robert C. Martin", "year": 2008 }
You returned a Java object and got JSON. Notice the field names match your record components exactly — that's Jackson mapping each accessor to a JSON key. The id comes out as a quoted string: that's how Jackson renders a UUID, since JSON has no native UUID type.
APIs rarely return one thing. Add a second method that returns a List<Book> — Spring serialises collections to a JSON array just as readily:
src/main/java/com/example/library/book/BookController.java (add the import + method)
import java.util.List;
import java.util.UUID;
// ...inside the class:
@GetMapping("/api/books")
public List<Book> all() {
return List.of(
new Book(UUID.fromString("11111111-1111-1111-1111-111111111111"), "Clean Code", "Robert C. Martin", 2008),
new Book(UUID.fromString("22222222-2222-2222-2222-222222222222"), "Effective Java", "Joshua Bloch", 2018),
new Book(UUID.fromString("33333333-3333-3333-3333-333333333333"), "The Pragmatic Programmer", "Hunt & Thomas", 1999)
);
} Restart and open http://localhost:8080/api/books:
[
{ "id": "11111111-1111-1111-1111-111111111111", "title": "Clean Code", "author": "Robert C. Martin", "year": 2008 },
{ "id": "22222222-2222-2222-2222-222222222222", "title": "Effective Java", "author": "Joshua Bloch", "year": 2018 },
{ "id": "33333333-3333-3333-3333-333333333333", "title": "The Pragmatic Programmer", "author": "Hunt & Thomas", "year": 1999 }
] The data is hard-coded for now — that's deliberate. We're focused on the web layer (request in, JSON out). A real data store arrives in Lesson 5 (Spring Data JPA); the layer that holds this list moves into a service in Lesson 3.
Both routes start with /api/books. Repeating that on every method is noise — and a typo waiting to happen. Lift the shared prefix to the class with @RequestMapping, and let each method declare only the part that's unique to it:
src/main/java/com/example/library/book/BookController.java
package com.example.library.book;
import java.util.List;
import java.util.UUID;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/books") // base path for every method in this class
public class BookController {
@GetMapping // GET /api/books
public List<Book> all() {
return List.of(
new Book(UUID.fromString("11111111-1111-1111-1111-111111111111"), "Clean Code", "Robert C. Martin", 2008),
new Book(UUID.fromString("22222222-2222-2222-2222-222222222222"), "Effective Java", "Joshua Bloch", 2018),
new Book(UUID.fromString("33333333-3333-3333-3333-333333333333"), "The Pragmatic Programmer", "Hunt & Thomas", 1999)
);
}
@GetMapping("/featured") // GET /api/books/featured
public Book featured() {
return new Book(UUID.fromString("11111111-1111-1111-1111-111111111111"),
"Clean Code", "Robert C. Martin", 2008);
}
}
Spring concatenates the class-level path with each method-level path. The endpoints behave exactly as before, but the routing now reads cleanly: this class owns the /api/books resource. The /api prefix is a common convention that keeps your data endpoints distinct from any pages or static files.
Content-Type: application/json header. This is the same @ResponseBody machinery from Lesson 1 — a String just took a different, plain-text converter. The reverse direction (JSON in → Java object) is deserialization, which you'll meet in Lesson 4 when clients POST new books.
The browser shows JSON, but it hides the headers. Use curl -i from the terminal to see the full HTTP response — status line, headers, and body:
curl -i http://localhost:8080/api/books HTTP/1.1 200
Content-Type: application/json
...
[{"id":"11111111-1111-1111-1111-111111111111","title":"Clean Code",...}]
That Content-Type: application/json is the proof: Spring chose the JSON converter for you. Deciding which format to send (here, JSON) based on the response type and the client's request is called content negotiation.
or missing fields? Jackson serialises via accessors. With a record you get them automatically; with a plain class, make sure it has public getters (or public fields), or Jackson sees nothing to write./api/books? Check the class-level @RequestMapping("/api/books") and that the method's @GetMapping has no leading duplicate (use @GetMapping with no value for the base path itself, not @GetMapping("/api/books") again).java.util.List) or a typo in the package line is the usual culprit.Retrieval beats re-reading. Answer from memory before clicking.
Book object from a @RestController method. What turns it into JSON?@RequestMapping("/api/books") on the class do?List<Book> and what does the client receive?Book? Primary source: the Spring reference on
@ResponseBody
and
HTTP message conversion
— this is the official description of the exact mechanism you just used. Short and worth skimming to cement how return values become JSON.
New vocabulary from this lesson lives in the Glossary — your quick-reference for every term we use.