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-parent pins 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-web pulls 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:tree answers — the technique from this track's Maven vs Gradle post.
  • java.version 25 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 the spring-boot-web-server and spring-boot-tomcat modules — 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:

curl GET /api/orders/ ORD-42/status Tomcat :8080 embedded, in your JVM Dispatcher Servlet routes by path OrderStatusController .status("ORD-42") → JSON Boot assembled every box except the last one — that one is yours

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 id works.
  • Or be explicit — @PathVariable("id") String id never 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:

spring-webmvc on classpath? came in via starter-web DispatcherServlet bean MATCHED routes every request tomcat classes on classpath? came in via starter-tomcat Tomcat server factory bean MATCHED listens on 8080 jackson on classpath? came in via starter-jackson JSON message converter bean MATCHED record → {"orderId": ...}

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

Popular posts from this blog

JSP Servlet Interview Questions For Freshers Series 1

Java Banking Finance Services and Insurance (BFSI) domain interview questions

Java program to check even or odd number