Creating REST APIs in Spring Boot: A Complete Step-by-Step Guide
This chapter covers the foundations of building RESTful Web Services in Spring Boot by constructing the core backend for an end-to-end Journaling Application (journal-app). You will learn how to bootstrap a Spring Boot project, understand the mechanics of REST architectural principles, implement CRUD operations using Spring Web annotations (@RestController, @RequestMapping, @GetMapping, @PostMapping, @PutMapping, @DeleteMapping), and handle incoming request payloads and dynamic path variables using an in-memory data store.
1. Project Setup & Architecture Mechanics
Before writing API handlers, we bootstrap a clean Spring Boot project. We use the Spring Initializr tool with deliberate architectural choices regarding versioning, naming standards, and dependencies.
1.1 Initialization Parameters
1.2 Refactoring Main Application Class
When generating the project, Spring Initializr defaults the entrypoint class to JournalAppApplication. To follow clean naming conventions, use your IDE's Refactor -> Rename tool (e.g., in IntelliJ IDEA) to rename it to JournalApplication.
Note: Always use the IDE's built-in Refactor tool rather than manual file renaming. Refactoring automatically updates class references across the entire codebase, XML configs, and annotations without breaking application wiring.
package net.engineeringdigest.journalApp;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class JournalApplication {
public static void main(String[] args) {
SpringApplication.run(JournalApplication.class, args);
}
}
2. Deconstructing REST API Principles
REST stands for Representational State Transfer, and API stands for Application Programming Interface. A RESTful API provides a standardized protocol for clients (mobile apps, web browsers, frontend frameworks like React) to interact with server resources over HTTP.
2.1 The Core Formula of a REST API
A REST API request is fundamentally composed of two core elements:
For example, navigating to http://localhost:8080/journal alone is incomplete without specifying the HTTP method verb that dictates the intended action on that resource.
2.2 HTTP Verbs Synthesis
3. Spring Web Annotations & Controller Architecture
In Spring Boot, controllers process incoming HTTP requests and format HTTP responses. The primary annotation powering this component is @RestController.
3.1 @RestController vs @Controller
@Component: The generic stereotype annotation for any Spring-managed Bean inside the IoC Container.@Controller: A specialized@Componentdesigned for web layer request handling, typically returning HTML view templates (e.g., Thymeleaf/JSP).@RestController: A compound annotation consisting of@Controller+@ResponseBody. It automatically serializes return values from handler methods into JSON or XML format directly in the HTTP response body.
3.2 Simple Health Check Endpoint
To verify that our web server running on port 8080 is properly intercepting requests, we construct a simple controller:
package net.engineeringdigest.journalApp.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HealthCheckController {
@GetMapping("/health-check")
public String healthCheck() {
return "OK";
}
}
When hitting GET http://localhost:8080/health-check, Spring MVC maps the request path /health-check directly to healthCheck(), returning "OK" with an HTTP 200 OK status code.
4. Designing the Domain Entity (POJO)
Before building resource endpoints for our Journaling Application, we create a Plain Old Java Object (POJO) representing a single journal entry.
4.1 JournalEntry Entity
Each entry contains an id, a title, and content.
package net.engineeringdigest.journalApp.entity;
public class JournalEntry {
private long id;
private String title;
private String content;
// Default Constructor
public JournalEntry() {}
// Parameterized Constructor
public JournalEntry(long id, String title, String content) {
this.id = id;
this.title = title;
this.content = content;
}
// Getters and Setters
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
5. Implementing In-Memory CRUD REST APIs
Before integrating a persistent database like MongoDB or PostgreSQL, we use an in-memory Map<Long, JournalEntry> within the controller class to simulate database operations.
5.1 Architecture of JournalEntryController
We apply @RequestMapping("/journal") at the class level. This prefixes every handler method inside this controller with the /journal base path.
package net.engineeringdigest.journalApp.controller;
import net.engineeringdigest.journalApp.entity.JournalEntry;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/journal")
public class JournalEntryController {
// Simulated in-memory database table
private Map<Long, JournalEntry> journalEntries = new HashMap<>();
// 1. GET ALL: GET http://localhost:8080/journal
@GetMapping
public List<JournalEntry> getAll() {
return new ArrayList<>(journalEntries.values());
}
// 2. CREATE: POST http://localhost:8080/journal
@PostMapping
public boolean createEntry(@RequestBody JournalEntry myEntry) {
journalEntries.put(myEntry.getId(), myEntry);
return true;
}
// 3. GET BY ID: GET http://localhost:8080/journal/id/{myId}
@GetMapping("/id/{myId}")
public JournalEntry getJournalEntryById(@PathVariable Long myId) {
return journalEntries.get(myId);
}
// 4. DELETE BY ID: DELETE http://localhost:8080/journal/id/{myId}
@DeleteMapping("/id/{myId}")
public JournalEntry deleteJournalEntryById(@PathVariable Long myId) {
return journalEntries.remove(myId);
}
// 5. UPDATE BY ID: PUT http://localhost:8080/journal/id/{myId}
@PutMapping("/id/{myId}")
public JournalEntry updateJournalEntryById(@PathVariable Long myId, @RequestBody JournalEntry myEntry) {
journalEntries.put(myId, myEntry);
return myEntry;
}
}
5.2 Deep Dive: Spring Request Handling Annotations
@RequestBody
Tells Spring Boot to deserialize the incoming JSON body from the HTTP request directly into a Java instance of JournalEntry using Jackson.
- Sample Incoming JSON Payload (POST / PUT):
{
"id": 1,
"title": "Morning Reflections",
"content": "Started the day with learning Spring Boot REST APIs!"
}
@PathVariable
Extracts dynamic parameters directly from the URI path. In @GetMapping("/id/{myId}"), {myId} defines a template variable. Adding @PathVariable Long myId in the method signature binds whatever value is passed in the URL (e.g., /journal/id/2) to the myId Java parameter.
6. Request Parameters vs. Path Variables
When designing API endpoints to accept dynamic inputs, developers choose between Path Variables and Query Request Parameters.
6.1 Comparison Matrix
Real-Life Analogy
Imagine a postal mailbox system:
- Path Variable refers directly to house numbers on a street (
/street/42). You are addressing a specific structure.- Query Parameter is like requesting mail filtered by criteria (
/mailboxes?unread=true). You are applying search/filter criteria to a collection.
7. Common Pitfalls, Edge Cases & Debugging
During development and manual testing with tools like Postman, several common configuration errors can cause request failures or runtime crashes.
7.1 Conflicting Endpoints (Ambiguous Mapping)
If two controller methods share the exact same URL path and the same HTTP Verb within the same class context, Spring Boot will fail during startup with an IllegalStateException / Ambiguous mapping error.
// BAD PRACTICE: Duplicate Mapping Conflict
@GetMapping
public List<JournalEntry> getAll() { ... }
@GetMapping
public String getCustomMessage() { ... } // CRASH: Spring Boot will fail to start!
Resolution: Ensure every handler method has either a unique combination of URI path template or a distinct HTTP method verb (
GET,POST,PUT,DELETE).
7.2 HTTP 405 Method Not Allowed
If a client sends an HTTP POST request to an endpoint configured exclusively with @GetMapping, Spring Boot returns an HTTP 405 Method Not Allowed response.
7.3 Testing Workflow with Postman
POST /journal: Populate entries into the in-memoryHashMap.GET /journal: Verify the entries are returned as a JSON array.GET /journal/id/1: Fetch a specific entry by path variable key.PUT /journal/id/1: Send updated JSON fields in the request body to modify the record.DELETE /journal/id/1: Remove the record from memory and confirm via a subsequentGET /journalcall.
8. Summary and Key Takeaways
- Project Bootstrapping: Select stable Spring Boot versions (e.g.,
2.7.x) to leverage rich community support and avoid unexpected dependency issues while learning fundamentals. - REST Core Formula: A RESTful API endpoint is defined as an HTTP Verb + Resource URL.
- Role of
@RestController: Combines@Controllerand@ResponseBody, ensuring that Java return types are automatically converted into JSON responses via embedded Jackson serialization. - Class-Level Mapping: Use
@RequestMapping("/journal")on controller classes to define standard base paths and maintain clean endpoint paths. - Payload Serialization: Use
@RequestBodyto automatically deserialize incoming HTTP request JSON payloads into Java domain objects (POJOs). - Dynamic Route Matching: Use
@PathVariablein combination with URI templates (e.g.,"/id/{myId}") to extract variables directly from URL paths. - HTTP Method Separation: Differing HTTP Verbs (
GET,POST,PUT,DELETE) operating on identical URI paths execute distinct operations without endpoint collision. - In-Memory Prototyping: Utilizing thread-safe structures or standard collection maps (
HashMap) provides an effective temporary strategy to validate API controller functionality prior to database integration.