Spring Boot 4 Quickstart: Your First REST API
The storefront team was blocked. They were building the "track your order" page for our checkout service and they needed one thing from the backend team: an HTTP endpoint that answers "what is the status of order ORD-42?" No database yet, no auth yet, no payment provider — just something that answers HTTP so the frontend can move. The backend estimate for "a service that serves HTTP" was two days: servlet container config, XML wiring, deployment descriptors. It took eleven minutes.
That gap — between what serving HTTP used to cost and what it costs now — is the entire pitch of Spring Boot. This post gets you from zero to a working REST API: a real project, a real controller, a real running server, and just enough understanding of the auto-configuration underneath that it never feels like magic.
The project in one pom
The fastest start is start.spring.io: pick Maven, Java 25, Spring Boot 4.1.x, add the Spring Web dependency, and download the zip. But the site is just a form over Maven coordinates, and you should know what those coordinates are. Here is the entire pom.xml — this is well-formed and verified against Maven Central:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.1</version>
</parent>
<groupId>com.javamakeuse</groupId>
<artifactId>checkout-service</artifactId>
<version>0.1.0</version>
<properties>
<java.version>25</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Three things to notice:
- The parent does the version management.
spring-boot-starter-parentpins compatible versions for every Spring artifact, Jackson, Tomcat, and the rest — notice the dependencies above declare no versions. You manage one version number (the parent's) instead of thirty. - Starters are bundles, not code.
spring-boot-starter-webpulls in Spring MVC, an embedded Tomcat, Jackson for JSON, and validation support in one coordinate. If you ever wonder "what did this starter actually bring?",mvn dependency:treeanswers — the technique from this track's Maven vs Gradle post. java.version25 matches this track's JDK. Spring Boot 4 requires Java 17 or newer — 25 is comfortably above the floor.
(Check for newer versions than the 4.1.1 pinned above — the coordinates are the stable part.)
Two files and you're serving HTTP
A Boot application needs exactly two things: an entry point and something to serve. The entry point:
package com.javamakeuse.checkout;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CheckoutApplication {
public static void main(String[] args) {
SpringApplication.run(CheckoutApplication.class, args);
}
}
@SpringBootApplication is three annotations in a trench coat: @Configuration (this class can declare beans), @EnableAutoConfiguration (turn on the conditional setup described below), and @ComponentScan (find my @RestController and friends in this package and below). One annotation, three jobs — and now you know which three.
The thing that serves — our order-status endpoint for the storefront team:
package com.javamakeuse.checkout.orders;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
@RestController
public class OrderStatusController {
public record OrderStatus(String orderId, String status, BigDecimal total) {}
@GetMapping("/api/orders/{id}/status")
public OrderStatus status(@PathVariable String id) {
// TODO: look the order up in the database (later post in this track)
return new OrderStatus(id, "PAID", new BigDecimal("129.99"));
}
}
Read it literally: @RestController says "this class handles web requests and its return values are response bodies, not view names." @GetMapping("/api/orders/{id}/status") says "HTTP GET on this path comes here, with {id} captured into the method argument." The returned record is serialized to JSON automatically — no manual JSON code anywhere. That hardcoded "PAID" is deliberate: get HTTP working first, wire the database in a later post. Working software in layers beats a big-bang integration.
Run it and read the console
mvn spring-boot:run
The console output is long, but only six lines matter. Here they are, abridged from a real run of exactly the code above (timestamps trimmed):
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v4.1.1)
... Starting CheckoutApplication using Java 21.0.12.1
... Tomcat initialized with port 8080 (http)
... Tomcat started on port 8080 (http) with context path '/'
... Root WebApplicationContext: initialization completed in 1883 ms
... Tomcat started on port 8080 (http) with context path '/'
... Started CheckoutApplication in 4.045 seconds (process running for 4.41)
What each line is telling you:
- The banner — vanity, but also a version check:
(v4.1.1)confirms which Boot actually booted. When a teammate says "it works on my machine," the banner is the first thing to compare. Tomcat initialized with port 8080— Boot started an embedded Tomcat inside your JVM. No separate server install, no WAR deployment. In Boot 4 this machinery lives in thespring-boot-web-serverandspring-boot-tomcatmodules — pulled in by the starter, never declared by you.Root WebApplicationContext: initialization completed— the Spring container finished wiring all beans (your controller included). If startup fails, it almost always fails here, and the lines above the failure name the bean that broke.Tomcat started on port 8080— the server is accepting connections. This is the line your deploy health-check is really waiting for.Started CheckoutApplication in 4.045 seconds— wall-clock startup on the author's machine. If this number triples after you add a dependency, that dependency did it.
And the payoff — the storefront team unblocked:
$ curl http://localhost:8080/api/orders/ORD-42/status
{"orderId":"ORD-42","status":"PAID","total":129.99}
Real HTTP, real JSON, eleven minutes. Here is the path a request takes through the pieces Boot assembled:
The -parameters gotcha (a real 500)
When this exact controller was first run during the preparation of this post, curl returned HTTP 500 with this root cause:
java.lang.IllegalArgumentException: Name for argument of type [java.lang.String]
not specified, and parameter name information not available via reflection.
Ensure that the compiler uses the '-parameters' flag.
@PathVariable String id without an explicit name needs the Java parameter name (id) to survive compilation — and javac discards parameter names unless you pass -parameters. The fix in real builds: spring-boot-starter-parent already passes -parameters to the compiler for you, so the code above works as written under Maven. Two ways to be safe regardless of build tool:
- Trust the parent (Maven) — parameter names are preserved,
@PathVariable String idworks. - Or be explicit —
@PathVariable("id") String idnever depends on compiler flags. (The next post in this track uses the explicit form throughout.)
Decision rule: an HTTP 500 whose message mentions -parameters is a build problem, not a code problem — check compiler flags before you "fix" the controller.
Auto-configuration: just enough magic
Here is the mental model that replaces magic: auto-configuration is a pile of if statements over your classpath. Each one says "if class X is present and the user hasn't defined their own Y, create a sensible Y." Three of them fired during our startup:
You can watch this happen. Run with the debug flag and Boot prints a CONDITIONS EVALUATION REPORT listing every auto-configuration and whether it matched. From the real run above:
$ mvn spring-boot:run -Dspring-boot.run.arguments=--debug
...
CONDITIONS EVALUATION REPORT
============================
...
DispatcherServletAutoConfiguration matched:
TomcatServletWebServerAutoConfiguration matched:
HttpMessageConvertersAutoConfiguration matched:
The report also lists what did not match and why — when Boot doesn't do what you expected, this report is the first place to look, not Stack Overflow.
Decision rule: never fight auto-configuration with XML or manual bean wiring — first check whether a condition (@ConditionalOnClass, @ConditionalOnMissingBean) explains the behavior. Nine times out of ten, the report tells you exactly which if fired.
One property to know
Almost everything Boot decides has a property override, kept in src/main/resources/application.properties. The one you'll reach for first:
server.port=8080
Change it to 8081, restart, and the "Tomcat started on port" line says 8081. That round-trip — property, restart, log line — is how you learn every other property in this track: the log always tells you what Boot actually did.
What's next
You can serve HTTP. The next post makes the API grown-up: reading request bodies and query parameters, rejecting bad input with Bean Validation, and turning every failure into clean, structured error JSON instead of a stack trace.
Field check before you move on: add a second endpoint to the controller — GET /api/orders/{id}/items returning a hard-coded list of two item names. Run it, curl it, then run once with --debug and find DispatcherServletAutoConfiguration in the conditions report. If you can do both, you own this post.
Continue: Java Learning Roadmap 2026
Comments
Post a Comment