Toolmingo
Guides18 min read

Spring Boot Tutorial for Beginners (2025): Learn Spring Boot Step by Step

Complete Spring Boot tutorial for absolute beginners. Learn REST APIs, Spring Data JPA, Spring Security, testing, and build a real project from scratch. Free guide with code examples.

Spring Boot is the most popular Java framework for building web applications and REST APIs. It powers applications at Netflix, Amazon, LinkedIn, and thousands of enterprises worldwide. Spring Boot removes the complexity of traditional Spring configuration and lets you build production-ready applications in minutes. This tutorial takes you from zero to a real Spring Boot application, step by step.

What you'll learn

Topic What you'll be able to do
Setup Create a Spring Boot project and run it locally
REST APIs Build GET, POST, PUT, DELETE endpoints
Spring Data JPA Connect to a database and perform CRUD operations
Validation Validate request data with annotations
Exception Handling Return proper error responses
Spring Security Add authentication to your API
Testing Write unit and integration tests
Deployment Package and run your app as a JAR

Why Spring Boot?

Traditional Spring required XML configuration files and extensive setup. Spring Boot eliminates that with auto-configuration and convention over configuration:

Feature Traditional Spring Spring Boot
Configuration XML/Java config, many files Auto-configured, minimal config
Server Deploy WAR to Tomcat Embedded Tomcat (runs as JAR)
Dependencies Manual version management Starter POMs with managed versions
Startup Complex setup @SpringBootApplication + main()
Database setup Manual DataSource config Auto-configured from properties
Production features Manual setup Actuator (health, metrics, info)
Learning curve Steep Gradual

Setup

Prerequisites

You need:

  • Java 17 or later (Java 21 recommended)
  • Maven or Gradle build tool
  • An IDE: IntelliJ IDEA (recommended), Eclipse, or VS Code
  • Basic Java knowledge (classes, interfaces, annotations)

Check your Java version:

java -version
# Should print: openjdk version "17.0.x" or "21.0.x"

Create your first Spring Boot project

Option 1: Spring Initializr (easiest)

Go to start.spring.io and configure:

Field Value
Project Maven
Language Java
Spring Boot 3.3.x (latest stable)
Group com.example
Artifact myapp
Packaging Jar
Java 21

Add these dependencies:

  • Spring Web — REST APIs
  • Spring Data JPA — database access
  • H2 Database — in-memory database (for learning)

Click Generate, download the ZIP, unzip it, and open in your IDE.

Option 2: Spring Boot CLI

# Install Spring Boot CLI via SDKMAN
curl -s "https://get.sdkman.io" | bash
sdk install springboot

# Create project
spring init --dependencies=web,data-jpa,h2 --name=myapp myapp.zip
unzip myapp.zip -d myapp
cd myapp

Project structure

myapp/
├── src/
│   ├── main/
│   │   ├── java/com/example/myapp/
│   │   │   └── MyappApplication.java    ← entry point
│   │   └── resources/
│   │       ├── application.properties   ← configuration
│   │       └── static/                  ← static files
│   └── test/
│       └── java/com/example/myapp/
│           └── MyappApplicationTests.java
├── pom.xml                              ← Maven dependencies
└── mvnw                                 ← Maven wrapper

Run the application

./mvnw spring-boot:run
# Or on Windows:
mvnw.cmd spring-boot:run

You'll see:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
...
Started MyappApplication in 1.823 seconds

Your app is running on http://localhost:8080.


Your first REST endpoint

The main class

Spring Boot generates this entry point:

package com.example.myapp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication   // enables auto-configuration + component scan
public class MyappApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyappApplication.class, args);
    }
}

@SpringBootApplication is a shortcut for three annotations:

  • @Configuration — marks this as a configuration class
  • @EnableAutoConfiguration — enables Spring Boot auto-configuration
  • @ComponentScan — scans for components in the package

Create a controller

Create HelloController.java:

package com.example.myapp;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController              // combines @Controller + @ResponseBody
@RequestMapping("/api")      // base path for all endpoints
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "Hello, Spring Boot!";
    }
}

Run the app and test:

curl http://localhost:8080/api/hello
# Output: Hello, Spring Boot!

Common annotations

Annotation Purpose
@RestController Marks class as REST controller (returns JSON by default)
@Controller Marks class as MVC controller (returns view names)
@GetMapping Handles HTTP GET requests
@PostMapping Handles HTTP POST requests
@PutMapping Handles HTTP PUT requests
@DeleteMapping Handles HTTP DELETE requests
@RequestMapping General-purpose mapping (all HTTP methods)
@PathVariable Binds URL path segment to parameter
@RequestParam Binds query parameter to method parameter
@RequestBody Binds request body (JSON) to object
@ResponseBody Writes return value directly to response

Building a REST API

Let's build a complete CRUD API for managing books.

The model (entity)

package com.example.myapp.model;

public class Book {
    private Long id;
    private String title;
    private String author;
    private int year;

    // Constructor
    public Book(Long id, String title, String author, int year) {
        this.id = id;
        this.title = title;
        this.author = author;
        this.year = year;
    }

    // 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 getAuthor() { return author; }
    public void setAuthor(String author) { this.author = author; }
    public int getYear() { return year; }
    public void setYear(int year) { this.year = year; }
}

The service layer

package com.example.myapp.service;

import com.example.myapp.model.Book;
import org.springframework.stereotype.Service;

import java.util.*;

@Service   // marks as service bean — Spring manages the lifecycle
public class BookService {

    private final Map<Long, Book> books = new HashMap<>();
    private long nextId = 1;

    public BookService() {
        // Seed some data
        books.put(1L, new Book(1L, "Clean Code", "Robert Martin", 2008));
        books.put(2L, new Book(2L, "The Pragmatic Programmer", "David Thomas", 1999));
        nextId = 3;
    }

    public List<Book> findAll() {
        return new ArrayList<>(books.values());
    }

    public Optional<Book> findById(Long id) {
        return Optional.ofNullable(books.get(id));
    }

    public Book create(Book book) {
        book.setId(nextId++);
        books.put(book.getId(), book);
        return book;
    }

    public Optional<Book> update(Long id, Book updated) {
        if (!books.containsKey(id)) return Optional.empty();
        updated.setId(id);
        books.put(id, updated);
        return Optional.of(updated);
    }

    public boolean delete(Long id) {
        return books.remove(id) != null;
    }
}

The controller

package com.example.myapp.controller;

import com.example.myapp.model.Book;
import com.example.myapp.service.BookService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/books")
public class BookController {

    private final BookService bookService;

    // Constructor injection (preferred over @Autowired)
    public BookController(BookService bookService) {
        this.bookService = bookService;
    }

    // GET /api/books
    @GetMapping
    public List<Book> getAll() {
        return bookService.findAll();
    }

    // GET /api/books/1
    @GetMapping("/{id}")
    public ResponseEntity<Book> getById(@PathVariable Long id) {
        return bookService.findById(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }

    // POST /api/books
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Book create(@RequestBody Book book) {
        return bookService.create(book);
    }

    // PUT /api/books/1
    @PutMapping("/{id}")
    public ResponseEntity<Book> update(@PathVariable Long id, @RequestBody Book book) {
        return bookService.update(id, book)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }

    // DELETE /api/books/1
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable Long id) {
        if (bookService.delete(id)) {
            return ResponseEntity.noContent().build();
        }
        return ResponseEntity.notFound().build();
    }
}

Test with curl

# Get all books
curl http://localhost:8080/api/books

# Get book by ID
curl http://localhost:8080/api/books/1

# Create a book
curl -X POST http://localhost:8080/api/books \
  -H "Content-Type: application/json" \
  -d '{"title":"Effective Java","author":"Joshua Bloch","year":2018}'

# Update a book
curl -X PUT http://localhost:8080/api/books/1 \
  -H "Content-Type: application/json" \
  -d '{"title":"Clean Code 2nd Ed","author":"Robert Martin","year":2024}'

# Delete a book
curl -X DELETE http://localhost:8080/api/books/1

Spring Data JPA

So far we've used an in-memory Map. Now let's use a real database with Spring Data JPA.

Add dependencies

In pom.xml (already included if you selected them at start.spring.io):

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

For production, replace H2 with MySQL or PostgreSQL:

<!-- MySQL -->
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

<!-- PostgreSQL -->
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <scope>runtime</scope>
</dependency>

Configure the database

In src/main/resources/application.properties:

# H2 in-memory database (for development)
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

# JPA settings
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop   # auto-create tables
spring.jpa.show-sql=true                     # log SQL queries

# H2 console (visit http://localhost:8080/h2-console)
spring.h2.console.enabled=true

For MySQL/PostgreSQL:

spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=root
spring.datasource.password=yourpassword
spring.jpa.hibernate.ddl-auto=update
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect

The JPA entity

package com.example.myapp.model;

import jakarta.persistence.*;

@Entity              // maps class to a database table
@Table(name = "books")
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)  // auto-increment
    private Long id;

    @Column(nullable = false)
    private String title;

    @Column(nullable = false)
    private String author;

    private int year;

    // Default constructor required by JPA
    public Book() {}

    public Book(String title, String author, int year) {
        this.title = title;
        this.author = author;
        this.year = year;
    }

    // Getters and setters
    public Long getId() { return id; }
    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }
    public String getAuthor() { return author; }
    public void setAuthor(String author) { this.author = author; }
    public int getYear() { return year; }
    public void setYear(int year) { this.year = year; }
}

The repository

Spring Data JPA generates all SQL automatically:

package com.example.myapp.repository;

import com.example.myapp.model.Book;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface BookRepository extends JpaRepository<Book, Long> {
    // JpaRepository provides: findAll, findById, save, deleteById, count, etc.

    // Spring generates SQL from method names automatically:
    List<Book> findByAuthor(String author);
    List<Book> findByYearGreaterThan(int year);
    List<Book> findByTitleContainingIgnoreCase(String keyword);
}

Method naming convention

Method name Generated SQL
findByTitle(String title) WHERE title = ?
findByAuthorAndYear(String author, int year) WHERE author = ? AND year = ?
findByYearBetween(int start, int end) WHERE year BETWEEN ? AND ?
findByTitleContaining(String text) WHERE title LIKE %?%
findByYearGreaterThan(int year) WHERE year > ?
findByTitleOrderByAuthorAsc(String title) WHERE title = ? ORDER BY author ASC
countByAuthor(String author) SELECT COUNT(*) WHERE author = ?
deleteByAuthor(String author) DELETE WHERE author = ?

Custom queries with @Query

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

// JPQL (object-oriented SQL)
@Query("SELECT b FROM Book b WHERE b.year >= :year ORDER BY b.title")
List<Book> findBooksFromYear(@Param("year") int year);

// Native SQL
@Query(value = "SELECT * FROM books WHERE title ILIKE %:keyword%", nativeQuery = true)
List<Book> searchByTitleNative(@Param("keyword") String keyword);

Updated service with JPA

package com.example.myapp.service;

import com.example.myapp.model.Book;
import com.example.myapp.repository.BookRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;

@Service
@Transactional   // all methods wrapped in a transaction by default
public class BookService {

    private final BookRepository bookRepository;

    public BookService(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }

    public List<Book> findAll() {
        return bookRepository.findAll();
    }

    public Optional<Book> findById(Long id) {
        return bookRepository.findById(id);
    }

    public Book create(Book book) {
        return bookRepository.save(book);
    }

    public Optional<Book> update(Long id, Book updated) {
        return bookRepository.findById(id).map(existing -> {
            existing.setTitle(updated.getTitle());
            existing.setAuthor(updated.getAuthor());
            existing.setYear(updated.getYear());
            return bookRepository.save(existing);
        });
    }

    public boolean delete(Long id) {
        if (!bookRepository.existsById(id)) return false;
        bookRepository.deleteById(id);
        return true;
    }

    public List<Book> findByAuthor(String author) {
        return bookRepository.findByAuthor(author);
    }
}

Validation

Add the dependency

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

Annotate your model

import jakarta.validation.constraints.*;

@Entity
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank(message = "Title is required")
    @Size(min = 1, max = 255, message = "Title must be between 1 and 255 characters")
    private String title;

    @NotBlank(message = "Author is required")
    private String author;

    @Min(value = 1000, message = "Year must be at least 1000")
    @Max(value = 2100, message = "Year must be at most 2100")
    private int year;
    
    // ... constructors, getters, setters
}

Enable validation in controller

import jakarta.validation.Valid;

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Book create(@Valid @RequestBody Book book) {  // @Valid triggers validation
    return bookService.create(book);
}

Common validation annotations

Annotation Description
@NotNull Field must not be null
@NotBlank String must not be null or blank (whitespace)
@NotEmpty String/collection must not be null or empty
@Size(min, max) String length or collection size within range
@Min(value) Numeric minimum value
@Max(value) Numeric maximum value
@Email Must be a valid email format
@Pattern(regexp) Must match regex
@Positive Must be positive number
@Future Date must be in the future
@Past Date must be in the past

Handle validation errors

package com.example.myapp.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice   // handles exceptions globally across all controllers
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationErrors(
            MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach(error -> {
            String field = ((FieldError) error).getField();
            String message = error.getDefaultMessage();
            errors.put(field, message);
        });
        return ResponseEntity.badRequest().body(errors);
    }
}

Now invalid requests return:

{
  "title": "Title is required",
  "year": "Year must be at least 1000"
}

Exception Handling

Custom exception

package com.example.myapp.exception;

public class BookNotFoundException extends RuntimeException {
    public BookNotFoundException(Long id) {
        super("Book not found with id: " + id);
    }
}

Throw from service

public Book findById(Long id) {
    return bookRepository.findById(id)
        .orElseThrow(() -> new BookNotFoundException(id));
}

Handle in global handler

import org.springframework.web.server.ResponseStatusException;

@ExceptionHandler(BookNotFoundException.class)
public ResponseEntity<Map<String, String>> handleBookNotFound(BookNotFoundException ex) {
    Map<String, String> error = Map.of("error", ex.getMessage());
    return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}

// Or use @ResponseStatus on the exception class:
// @ResponseStatus(HttpStatus.NOT_FOUND)
// public class BookNotFoundException extends RuntimeException { ... }

Configuration with application.properties

Spring Boot configuration lives in src/main/resources/application.properties or application.yml:

# Server
server.port=8080
server.servlet.context-path=/api   # prefix all endpoints with /api

# Database
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=postgres
spring.datasource.password=secret

# JPA
spring.jpa.hibernate.ddl-auto=update   # update (dev) | validate (prod) | none
spring.jpa.show-sql=false
spring.jpa.properties.hibernate.format_sql=true

# Logging
logging.level.com.example.myapp=DEBUG
logging.level.org.springframework.web=INFO
logging.level.org.hibernate.SQL=DEBUG

# Actuator
management.endpoints.web.exposure.include=health,info,metrics

Custom properties

# application.properties
app.name=My Book API
app.max-books=1000
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Service
public class BookService {

    @Value("${app.max-books}")
    private int maxBooks;

    @Value("${app.name}")
    private String appName;
}

Typed configuration with @ConfigurationProperties

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "app")
public class AppProperties {
    private String name;
    private int maxBooks;

    // Getters and setters required
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public int getMaxBooks() { return maxBooks; }
    public void setMaxBooks(int maxBooks) { this.maxBooks = maxBooks; }
}

Profiles

Use different configuration for dev vs production:

application.properties        ← shared config
application-dev.properties    ← dev-only config
application-prod.properties   ← production config
# application-dev.properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.jpa.show-sql=true
logging.level.com.example=DEBUG

# application-prod.properties
spring.datasource.url=jdbc:postgresql://prod-host:5432/mydb
spring.jpa.show-sql=false
logging.level.com.example=WARN

Activate a profile:

# Command line
java -jar myapp.jar --spring.profiles.active=prod

# Environment variable
SPRING_PROFILES_ACTIVE=prod java -jar myapp.jar

# application.properties (default)
spring.profiles.active=dev

Spring Security Basics

Add dependency

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Adding this dependency immediately secures all endpoints with HTTP Basic Auth. The auto-generated password is printed in the console:

Using generated security password: 3e8f9a2c-4b1d-4e2f-8c3d-1a2b3c4d5e6f

Basic security configuration

package com.example.myapp.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/books").permitAll()       // public endpoint
                .requestMatchers("/api/admin/**").hasRole("ADMIN") // admin only
                .anyRequest().authenticated()                    // all others: login required
            )
            .httpBasic(httpBasic -> {})   // enable HTTP Basic Auth
            .csrf(csrf -> csrf.disable()); // disable CSRF for REST APIs
        return http.build();
    }

    @Bean
    public UserDetailsService userDetailsService(PasswordEncoder encoder) {
        var user = User.withUsername("user")
            .password(encoder.encode("password"))
            .roles("USER")
            .build();
        var admin = User.withUsername("admin")
            .password(encoder.encode("adminpass"))
            .roles("USER", "ADMIN")
            .build();
        return new InMemoryUserDetailsManager(user, admin);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

Security comparison

Approach Best for
HTTP Basic Simple internal APIs, quick prototyping
Session-based Traditional web apps with server-side sessions
JWT Stateless REST APIs, mobile apps, microservices
OAuth2 Third-party login (Google, GitHub)
API Key Machine-to-machine communication

Testing

Unit tests

package com.example.myapp.service;

import com.example.myapp.exception.BookNotFoundException;
import com.example.myapp.model.Book;
import com.example.myapp.repository.BookRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Optional;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class BookServiceTest {

    @Mock
    private BookRepository bookRepository;

    @InjectMocks
    private BookService bookService;

    private Book book;

    @BeforeEach
    void setUp() {
        book = new Book("Clean Code", "Robert Martin", 2008);
        book.setId(1L);
    }

    @Test
    void findById_returnsBook_whenExists() {
        when(bookRepository.findById(1L)).thenReturn(Optional.of(book));

        Book result = bookService.findById(1L);

        assertThat(result.getTitle()).isEqualTo("Clean Code");
        verify(bookRepository).findById(1L);
    }

    @Test
    void findById_throwsException_whenNotFound() {
        when(bookRepository.findById(99L)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> bookService.findById(99L))
            .isInstanceOf(BookNotFoundException.class)
            .hasMessageContaining("99");
    }

    @Test
    void create_savesAndReturnsBook() {
        when(bookRepository.save(book)).thenReturn(book);

        Book result = bookService.create(book);

        assertThat(result).isEqualTo(book);
        verify(bookRepository).save(book);
    }
}

Integration tests

package com.example.myapp.controller;

import com.example.myapp.model.Book;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringBootTest
@AutoConfigureMockMvc
class BookControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    void getAll_returnsOk() throws Exception {
        mockMvc.perform(get("/api/books"))
            .andExpect(status().isOk())
            .andExpect(content().contentType(MediaType.APPLICATION_JSON));
    }

    @Test
    void getById_returnsNotFound_whenMissing() throws Exception {
        mockMvc.perform(get("/api/books/99999"))
            .andExpect(status().isNotFound());
    }

    @Test
    void create_returnsCreated_withValidBook() throws Exception {
        Book book = new Book("New Book", "Author", 2025);

        mockMvc.perform(post("/api/books")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(book)))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.title").value("New Book"))
            .andExpect(jsonPath("$.id").isNumber());
    }
}

Test annotations reference

Annotation Purpose
@SpringBootTest Loads full application context
@WebMvcTest Loads only web layer (faster)
@DataJpaTest Loads only JPA layer with in-memory DB
@MockBean Creates a mock Spring bean
@AutoConfigureMockMvc Auto-configures MockMvc
@Transactional on test Rolls back DB changes after each test
@BeforeEach Runs before each test method
@AfterEach Runs after each test method

Spring Boot Actuator

Actuator exposes built-in endpoints for monitoring:

Enable Actuator

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
# Expose endpoints
management.endpoints.web.exposure.include=health,info,metrics,env
management.endpoint.health.show-details=always
management.info.env.enabled=true

Available endpoints

Endpoint URL Description
/actuator/health GET /actuator/health App health status
/actuator/info GET /actuator/info App info (version, etc.)
/actuator/metrics GET /actuator/metrics JVM + app metrics
/actuator/env GET /actuator/env Environment variables
/actuator/beans GET /actuator/beans All Spring beans
/actuator/mappings GET /actuator/mappings All request mappings
/actuator/loggers GET /actuator/loggers Logger levels
curl http://localhost:8080/actuator/health
# {"status":"UP","components":{"db":{"status":"UP"},"diskSpace":{"status":"UP"}}}

Packaging and Deployment

Build a JAR

./mvnw clean package -DskipTests
# Creates: target/myapp-0.0.1-SNAPSHOT.jar

Run the JAR

java -jar target/myapp-0.0.1-SNAPSHOT.jar
java -jar target/myapp-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod
java -jar target/myapp-0.0.1-SNAPSHOT.jar --server.port=9000

Dockerfile

FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY target/myapp-0.0.1-SNAPSHOT.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
docker build -t myapp .
docker run -p 8080:8080 -e SPRING_PROFILES_ACTIVE=prod myapp

Multi-stage Dockerfile (build inside Docker)

# Build stage
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn clean package -DskipTests

# Run stage
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

Common Spring Boot patterns

Dependency Injection

Spring manages bean lifecycle and wires dependencies automatically:

// Three ways to inject dependencies — constructor injection is preferred
@Service
public class BookService {

    // 1. Constructor injection (recommended — immutable, easy to test)
    private final BookRepository repo;
    public BookService(BookRepository repo) {
        this.repo = repo;
    }

    // 2. Field injection (convenient, but hard to test)
    @Autowired
    private BookRepository repo2;

    // 3. Setter injection (rarely used)
    @Autowired
    public void setRepo(BookRepository repo3) { this.repo = repo3; }
}

Lombok (reduce boilerplate)

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
import lombok.*;

@Entity
@Data               // @Getter + @Setter + @ToString + @EqualsAndHashCode
@NoArgsConstructor  // default constructor
@AllArgsConstructor // constructor with all fields
@Builder            // builder pattern
public class Book {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;
    private String author;
    private int year;
}

// Usage with Builder
Book book = Book.builder()
    .title("Clean Code")
    .author("Robert Martin")
    .year(2008)
    .build();

DTOs (Data Transfer Objects)

Avoid exposing JPA entities directly — use DTOs:

// Request DTO (input)
public record CreateBookRequest(
    @NotBlank String title,
    @NotBlank String author,
    @Min(1000) int year
) {}

// Response DTO (output)
public record BookResponse(Long id, String title, String author, int year) {
    public static BookResponse from(Book book) {
        return new BookResponse(book.getId(), book.getTitle(), book.getAuthor(), book.getYear());
    }
}

// Controller using DTOs
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public BookResponse create(@Valid @RequestBody CreateBookRequest request) {
    Book book = new Book(request.title(), request.author(), request.year());
    return BookResponse.from(bookService.create(book));
}

Spring Boot vs alternatives

Feature Spring Boot Quarkus Micronaut Express.js (Node)
Language Java/Kotlin Java/Kotlin Java/Kotlin/Groovy JavaScript
Startup time ~2-3s ~0.05s ~0.3s ~0.1s
Memory usage ~200MB ~60MB ~80MB ~50MB
Ecosystem Huge (Spring) Growing Growing Huge (npm)
Learning curve Moderate Moderate Moderate Easy
GraalVM native Yes (limited) First-class First-class N/A
Enterprise use Dominant Growing Growing Common
Community Largest Growing Growing Largest

Common mistakes

Mistake Problem Fix
Using @Autowired on fields Hard to test, hides dependencies Use constructor injection
Exposing JPA entities in API Leaks internal structure, N+1 queries Use DTOs
Not handling transactions Data inconsistency on errors Add @Transactional to service methods
spring.jpa.hibernate.ddl-auto=create in prod Drops and recreates all tables Use validate or none in prod, use Flyway/Liquibase
Storing passwords in plain text Critical security vulnerability Always use BCryptPasswordEncoder
Fat service classes Hard to maintain and test Apply Single Responsibility Principle
Catching and swallowing exceptions Silent failures, impossible to debug Log and re-throw or use global exception handler
Not using profiles Same config in dev and prod Use application-dev.properties + application-prod.properties

What to learn next

Topic What it adds
Spring Data REST Auto-generates REST API from repository
Spring Cache @Cacheable — cache method results in Redis/Ehcache
Spring Batch Process large datasets in batches
Spring WebFlux Reactive, non-blocking web framework
Flyway / Liquibase Database migration management
MapStruct Automatic entity↔DTO mapping
Spring Cloud Microservices: service discovery, config server, API gateway
Testcontainers Run real databases in Docker during tests

Frequently asked questions

Do I need to know Spring before learning Spring Boot?

No. Spring Boot is the recommended entry point. You'll learn Spring concepts (IoC, dependency injection, beans) naturally as you build with Spring Boot.

Spring Boot vs Spring MVC — what's the difference?

Spring MVC is the web framework inside Spring. Spring Boot is a tool that auto-configures Spring MVC (and everything else) for you. Spring Boot uses Spring MVC under the hood.

Which Java version should I use with Spring Boot?

Spring Boot 3.x requires Java 17 minimum. Java 21 (LTS) is recommended for new projects — it's faster, uses less memory, and includes virtual threads.

Is Spring Boot good for microservices?

Yes. Spring Boot is one of the most popular choices for microservices. Add Spring Cloud for service discovery (Eureka), API gateway (Spring Cloud Gateway), and distributed configuration.

How do I deploy Spring Boot to the cloud?

Package as a JAR (./mvnw package) and deploy to AWS Elastic Beanstalk, Google Cloud Run, Azure App Service, Heroku, or Railway. Or containerize with Docker and deploy to Kubernetes.

Spring Boot vs Node.js — which should I learn?

If your team uses Java (especially in enterprise or fintech), Spring Boot. If your team uses JavaScript end-to-end, Node.js. Both are excellent for building REST APIs. See our Node.js vs Python guide for broader context.

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools