Spring Boot Beginner Guide: Build a REST API Step by Step
Updated 28 Oct 2026 6 min read
On this page 14 sections
- 1Why Spring Boot
- 2What you will build
- 3Step 1: create the project
- 4Step 2: the entity (database table)
- 5Step 3: the repository
- 6Step 4: the controller (REST endpoints)
- 7Step 5: layering and DTOs
- 8Step 6: validation and error handling
- 9Step 7: configuration and databases
- 10Step 8: testing
- 11Step 9: security and next steps
- 12Core concepts interviewers ask (with short answers)
- 13Common mistakes
- 14Next steps
Why Spring Boot
Spring Boot is the most widely used Java framework for building backend services. Banks, large IT companies and many product teams use it, so it appears in many Java and backend job listings. It removes much of the setup work: you get an embedded web server, sensible defaults and a huge ecosystem. Before you begin you should be comfortable with Java and OOP (Java guide) and the basics of HTTP and SQL (backend roadmap).
What you will build
A tiny task API with create, read, update and delete operations, stored in a database.
Step 1: create the project
Use Spring Initializr (start.spring.io) and choose: Maven or Gradle, Java (a currently supported LTS version), and the dependencies Spring Web, Spring Data JPA, Validation and an in-memory H2 Database for learning. Download, unzip and open it in your IDE. Run the main class annotated with @SpringBootApplication, and the server starts on port 8080.
Note: in Spring Boot 3 and later the persistence and validation annotations come from the jakarta.* packages (older tutorials show javax.*).
Step 2: the entity (database table)
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
@Entity
public class Task {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "Title is required")
private String title;
private boolean done;
// getters and setters (or use a Java record for DTOs)
}
@Entity maps the class to a table, @Id marks the primary key and the validation annotation checks incoming data.
Step 3: the repository
import org.springframework.data.jpa.repository.JpaRepository;
public interface TaskRepository extends JpaRepository<Task, Long> {
// Spring generates the implementation for you
// you can add queries by method name, for example:
// List<Task> findByDone(boolean done);
}
You get save, findById, findAll, deleteById and more without writing SQL.
Step 4: the controller (REST endpoints)
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/tasks")
public class TaskController {
private final TaskRepository repo;
public TaskController(TaskRepository repo) { // constructor injection
this.repo = repo;
}
@GetMapping
public List<Task> all() {
return repo.findAll();
}
@GetMapping("/{id}")
public ResponseEntity<Task> one(@PathVariable Long id) {
return repo.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<Task> create(@Valid @RequestBody Task task) {
return ResponseEntity.status(201).body(repo.save(task));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
if (!repo.existsById(id)) return ResponseEntity.notFound().build();
repo.deleteById(id);
return ResponseEntity.noContent().build();
}
}
Test it with a REST client such as Postman, or with curl: curl -X POST localhost:8080/api/tasks -H "Content-Type: application/json" -d '{"title":"Learn Spring"}'.
Step 5: layering and DTOs
As the app grows, split it into layers:
- Controller: handles HTTP and converts to or from DTOs
- Service: contains business rules and transactions (
@Service,@Transactional) - Repository: talks to the database
Do not expose entities directly to clients in real systems. Use separate request and response objects (DTOs) so that internal changes do not break the API.
Step 6: validation and error handling
Return clear error messages with a global handler:
import org.springframework.http.*;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestControllerAdvice
public class ApiErrors {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> invalid(MethodArgumentNotValidException ex) {
String msg = ex.getBindingResult().getFieldErrors().get(0).getDefaultMessage();
return ResponseEntity.badRequest().body(Map.of("error", msg));
}
}
Step 7: configuration and databases
Settings live in application.properties or application.yml. Use profiles (dev, prod) for different environments and keep secrets out of the repository. For real projects switch from H2 to PostgreSQL or MySQL by changing the dependency and the datasource settings, and manage schema changes with Flyway or Liquibase.
Step 8: testing
- Unit tests with JUnit and Mockito for service logic
- Slice or integration tests with
@WebMvcTestand@SpringBootTest - Always test validation failures and "not found" cases
Step 9: security and next steps
Learn Spring Security basics (authentication, password hashing, JWT or sessions), then caching, pagination (Pageable), logging, actuator health checks, Docker and deployment (Docker guide).
Core concepts interviewers ask (with short answers)
Inversion of Control (IoC) and dependency injection (DI). Instead of your class creating its dependencies (new EmailSender()), the Spring container creates objects (beans) and injects them. This makes code loosely coupled and easy to test with fakes. Prefer constructor injection (shown above): dependencies are required, can be final, and tests can pass them directly. Field injection with @Autowired hides dependencies and makes testing harder.
What @SpringBootApplication includes. It combines @Configuration (the class can define beans), @EnableAutoConfiguration (configure beans based on the libraries on the classpath and your properties) and @ComponentScan (find components in the package and below). Auto-configuration uses conditions such as "if a database driver is on the classpath and no DataSource bean exists, create one".
Stereotype annotations. @Component is the generic bean; @Service marks business logic; @Repository marks data access (and translates database exceptions); @Controller returns views, while @RestController is @Controller plus @ResponseBody, so return values are written as JSON.
Request handling annotations. @RequestMapping and the shortcuts @GetMapping, @PostMapping, @PutMapping, @DeleteMapping; @PathVariable (/tasks/{id}), @RequestParam (?page=2), @RequestBody (JSON body) and @Valid (run validation).
Bean scopes. Singleton (the default; one instance per container, so beans must be thread-safe and stateless), prototype (a new instance each time), and web scopes such as request and session. The bean lifecycle: instantiate, inject dependencies, run @PostConstruct, use, then @PreDestroy on shutdown.
Layers. Controller handles HTTP, service holds business rules and transactions, repository talks to the database. Use DTOs between layers so entities are not exposed directly.
JPA and Hibernate. JPA is the specification; Hibernate is the usual implementation. Entities map to tables. Know relationships (@OneToMany, @ManyToOne, @ManyToMany), lazy vs eager loading, and the N+1 query problem: loading a list of 100 parents and then issuing one extra query per parent to fetch children, giving 101 queries. Fix with a fetch join (JOIN FETCH), an entity graph or a DTO projection. Also know pagination with Pageable and why findAll() on a huge table is dangerous.
Transactions. @Transactional wraps a method in a database transaction: it commits on success and rolls back on unchecked exceptions by default (checked exceptions need rollbackFor). It works through proxies, so calling a @Transactional method from another method in the same class bypasses the proxy and the annotation has no effect. Know propagation (REQUIRED by default) at a basic level.
Exception handling. Use @RestControllerAdvice with @ExceptionHandler methods to return consistent error responses and the right status codes (400 for validation, 404 for missing, 409 for conflicts).
Configuration. application.properties or YAML, @Value and @ConfigurationProperties, profiles (dev, prod) and environment variables for secrets.
Spring Boot Actuator exposes health, metrics and info endpoints for monitoring. Spring Security provides authentication and authorisation; know the idea of filters, password hashing with BCrypt and stateless authentication with JWT.
Spring MVC flow. A request reaches the DispatcherServlet, which finds the handler (controller method) through handler mappings, calls it, converts the result to the response (JSON through message converters) and returns it.
Frequently asked, in one line each:
- What is Spring Boot, and how is it different from Spring? (opinionated setup, auto-configuration, embedded server, starters)
- What are starters? (curated dependency bundles such as
spring-boot-starter-web) - How does Spring Boot run without an external Tomcat? (an embedded server inside the jar)
@Componentvs@Bean? (class-level scanning vs a method in a@Configurationclass that creates a bean)- How do you handle exceptions globally?
- What is the N+1 problem, and how do you fix it?
- How do transactions work? Why might
@Transactionalnot work? - How do you secure a REST API?
- How do you test a Spring Boot application? (JUnit, Mockito,
@WebMvcTest,@DataJpaTest,@SpringBootTest) - How would you deploy it? (build a jar, containerise it, use profiles and externalised configuration)
Common mistakes
- Field injection and giant controllers with business logic inside
- Returning entities directly, including sensitive fields
- Ignoring validation and error responses
- Not learning SQL and relying only on repository magic (SQL guide)
Next steps
Turn this into a portfolio project with authentication, tests and a README (projects guide), and prepare for interviews with top Java questions.
Found this useful? Share it with a friend.