Spring Boot 3 Getting Started Guide — From Project Setup to REST API

Key Changes in Spring Boot 3

Spring Boot 3 requires Java 17 or higher and has transitioned to the Jakarta EE 10 namespace (javax.* to jakarta.*). It officially supports GraalVM native images, and observability has been unified through a Micrometer-based approach.

The fastest way to get started is by creating a project via Spring Initializr. The core dependency is spring-boot-starter-web, which alone includes embedded Tomcat, Jackson JSON processing, and Spring MVC.

Project Creation and Basic Structure

build.gradle Configuration

// build.gradle — Spring Boot 3 dependency configuration
plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.4'
    id 'io.spring.dependency-management' version '1.1.4'
}

group = 'com.example'
version = '1.0.0'

java {
    sourceCompatibility = '17' // Java 17 or higher required
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'       // REST API core
    implementation 'org.springframework.boot:spring-boot-starter-validation' // Input validation
    testImplementation 'org.springframework.boot:spring-boot-starter-test'  // Testing
}

Main Application Class

// Application.java — Spring Boot entry point
package com.example.demo;

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

@SpringBootApplication // Auto-configuration + component scanning + configuration class
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
        // Output:
        // Started Application in 1.234 seconds (process running for 1.567)
        // Tomcat started on port 8080
    }
}

@SpringBootApplication is a combination of three annotations. @EnableAutoConfiguration detects libraries on the classpath and applies auto-configuration. @ComponentScan automatically registers beans in the current package and sub-packages. @Configuration marks this class itself as a configuration class.

REST API Implementation

Domain Model and Controller

// TaskController.java — CRUD REST API implementation
package com.example.demo.controller;

import com.example.demo.dto.TaskRequest;
import com.example.demo.dto.TaskResponse;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

// DTO — concisely defined using records
record TaskRequest(
    @jakarta.validation.constraints.NotBlank(message = "Title is required")
    String title,
    String description
) {}

record TaskResponse(
    Long id,
    String title,
    String description,
    boolean completed,
    LocalDateTime createdAt
) {}

@RestController                    // Automatic JSON serialization
@RequestMapping("/api/tasks")      // Common path prefix
public class TaskController {

    // In-memory store (in practice, use a JPA Repository)
    private final Map<Long, TaskResponse> store = new ConcurrentHashMap<>();
    private final AtomicLong idGenerator = new AtomicLong(1);

    // GET /api/tasks — List all
    @GetMapping
    public List<TaskResponse> getAll() {
        return new ArrayList<>(store.values());
        // Response example: [{"id":1,"title":"Study","completed":false,...}]
    }

    // GET /api/tasks/{id} — Get single item
    @GetMapping("/{id}")
    public ResponseEntity<TaskResponse> getById(@PathVariable Long id) {
        TaskResponse task = store.get(id);
        if (task == null) {
            return ResponseEntity.notFound().build(); // Return 404
        }
        return ResponseEntity.ok(task); // 200 + JSON body
    }

    // POST /api/tasks — Create
    @PostMapping
    public ResponseEntity<TaskResponse> create(@Valid @RequestBody TaskRequest req) {
        Long id = idGenerator.getAndIncrement();
        TaskResponse task = new TaskResponse(
            id, req.title(), req.description(), false, LocalDateTime.now()
        );
        store.put(id, task);
        return ResponseEntity.status(HttpStatus.CREATED).body(task);
        // 201 Created + return created resource
    }

    // DELETE /api/tasks/{id} — Delete
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable Long id) {
        if (store.remove(id) == null) {
            return ResponseEntity.notFound().build();
        }
        return ResponseEntity.noContent().build(); // 204 No Content
    }
}

Global Exception Handling

// GlobalExceptionHandler.java — Unified error response
package com.example.demo.exception;

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

import java.time.LocalDateTime;
import java.util.List;

record ErrorResponse(int status, String message, LocalDateTime timestamp) {}

@RestControllerAdvice // Exception handler applied to all controllers
public class GlobalExceptionHandler {

    // Handle validation failures
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidation(
            MethodArgumentNotValidException ex) {

        List<String> errors = ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .map(e -> e.getField() + ": " + e.getDefaultMessage())
            .toList();

        ErrorResponse response = new ErrorResponse(
            400,
            String.join(", ", errors),
            LocalDateTime.now()
        );
        return ResponseEntity.badRequest().body(response);
        // Result: {"status":400,"message":"title: Title is required","timestamp":"..."}
    }
}

Understanding Auto-Configuration

Spring Boot’s auto-configuration follows the “Convention over Configuration” principle. When you add spring-boot-starter-web, the following is configured automatically.

Detection ConditionAuto-Configuration
Tomcat on classpathStarts embedded web server
Jackson on classpathJSON serialization/deserialization
@RestController presentSpring MVC dispatcher servlet
application.propertiesCustom settings for port, logging, etc.

You can override key settings in application.properties.

// application.properties — Key configuration items
// server.port=9090                    // Change port (default 8080)
// spring.jackson.date-format=yyyy-MM-dd // JSON date format
// logging.level.root=INFO             // Log level
// spring.profiles.active=dev          // Active profile

// Profile-specific configuration files
// application-dev.properties   -> Development environment
// application-prod.properties  -> Production environment

Profiles and External Configuration

// AppConfig.java — @ConfigurationProperties pattern
package com.example.demo.config;

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

@ConfigurationProperties(prefix = "app") // Binds app.* properties
public record AppConfig(
    String name,          // app.name
    int maxRetries,       // app.max-retries (kebab -> camel auto-conversion)
    String apiUrl         // app.api-url
) {}

// In application.properties:
// app.name=TaskManager
// app.max-retries=3
// app.api-url=https://api.example.com

// Usage example:
// @Service
// public class MyService {
//     private final AppConfig config;
//     public MyService(AppConfig config) {
//         this.config = config;
//         System.out.println(config.name()); // "TaskManager"
//     }
// }

Summary

Here are the key points to remember when starting with Spring Boot 3.

  • Java 17 or higher is required, and javax.* to jakarta.* migration is necessary
  • A single @SpringBootApplication integrates auto-configuration, component scanning, and configuration class
  • REST APIs are implemented with the @RestController + @RequestMapping combination, and using record for DTOs reduces boilerplate
  • @Valid and @RestControllerAdvice separate input validation from exception handling
  • Use @ConfigurationProperties for type-safe configuration binding
  • Manage environment-specific settings with profiles (spring.profiles.active)

The strength of Spring Boot is “start fast, scale flexibly.” Start quickly with default settings, and when needed, override auto-configuration for fine-grained control.

Was this article helpful?