Building Your First Spring Boot App: A Complete Guide to MVC Architecture and REST Controllers
Published · Updated
In a Spring Boot JSON API, Spring MVC maps an HTTP request to a controller method, converts request data into Java values, calls your application code, and converts the returned object into an HTTP response. This guide builds a tiny read-only task API so you can see that flow without a database obscuring it.
Version assumptions (updated 7 August 2026): Spring Boot 4.1.x, Java 17+ and a Maven project containing Spring Web. If you do not yet have a running project, follow Creating Your First Spring Application first.
MVC and REST are related, not identical
Spring MVC is the web framework that dispatches requests to controllers. MVC can
render server-side HTML views, but @RestController writes returned values to
the response body instead. In this example Jackson serializes Java records to
JSON because it is included by Spring Web.
Use this package layout under src/main/java/com/example/tasks:
com.example.tasks
├── TasksApplication.java
├── Task.java
├── TaskService.java
└── TaskController.java
Keep the generated TasksApplication class at the package root so component
scanning covers the controller and service.
1. Define the response resource
Create Task.java:
package com.example.tasks;
public record Task(long id, String title, boolean completed) {}
This record is the JSON representation returned to a client. A response such as
new Task(1, "Learn Spring MVC", false) becomes:
{"id":1,"title":"Learn Spring MVC","completed":false}
2. Put application work in a service
Create TaskService.java:
package com.example.tasks;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Service;
@Service
public class TaskService {
private final List<Task> tasks = List.of(
new Task(1, "Learn Spring MVC", false),
new Task(2, "Build a REST endpoint", true)
);
public List<Task> findAll() {
return tasks;
}
public Optional<Task> findById(long id) {
return tasks.stream().filter(task -> task.id() == id).findFirst();
}
}
This in-memory list is intentionally small and immutable. It demonstrates the service boundary; it is not a persistence pattern.
3. Map HTTP requests in the controller
Create TaskController.java:
package com.example.tasks;
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/tasks")
public class TaskController {
private final TaskService taskService;
public TaskController(TaskService taskService) {
this.taskService = taskService;
}
@GetMapping
public List<Task> list() {
return taskService.findAll();
}
@GetMapping("/{id}")
public ResponseEntity<Task> get(@PathVariable long id) {
return taskService.findById(id)
.map(ResponseEntity::ok)
.orElseGet(() -> ResponseEntity.notFound().build());
}
}
The class-level @RequestMapping supplies the common path. @GetMapping narrows
each method to HTTP GET. @PathVariable converts the {id} path segment to a
long. ResponseEntity lets the single-item endpoint explicitly choose either
200 OK with a body or 404 Not Found without one.
4. Run and call the API
./mvnw spring-boot:run
curl -i http://localhost:8080/api/tasks
curl -i http://localhost:8080/api/tasks/1
curl -i http://localhost:8080/api/tasks/99
The first two requests return 200. The missing id returns 404.
Trace one request
For GET /api/tasks/1:
- The embedded server accepts the HTTP request.
- Spring MVC’s
DispatcherServletselectsTaskController.get(...)from the registered mapping. - It converts the path text
1into thelongargument. - The controller delegates the lookup to
TaskService. - The controller builds the appropriate
ResponseEntity. - An HTTP message converter uses Jackson to write the
Taskas JSON.
The controller owns HTTP translation; the service owns the use-case logic. That separation is useful, but “one layer per noun” is not a goal. Introduce a layer when it gives a responsibility a clear home or protects a meaningful boundary.
What this first app deliberately omits
It does not yet accept untrusted input, mutate data, authenticate callers, or talk to a database. Those require additional decisions and tests. The advanced Spring Boot MVC and REST guide adds request DTO validation, creation status codes, and consistent error responses. Review IoC and dependency injection if the controller constructor is not yet clear.
Primary references
- Spring guide: Building a RESTful Web Service
- Spring Framework: annotated controllers
- Spring Framework: mapping requests
Layered service work is one foundation for the problem-solving scope described in the Forward-Deployed Engineer program.