This tutorial shows how to build a small validated JSON API with explicit status codes, service boundaries, and consistent errors. It targets Java 25 and favors complete, observable examples over isolated syntax fragments. You will create the files, run the commands, inspect the result, and test important failure paths.
The main topic is Spring Boot REST API. Keep controllers focused on HTTP, validate request DTOs, put business rules in services, and never expose persistence entities or exception traces directly.
Table of Contents
Prerequisites
Use a Java 25 JDK, not only a runtime, when compiling code. If Java is not installed, follow How to Install Java 25 on Ubuntu 24.04. Confirm the runtime and compiler before starting:
java --version
javac --version
printf "%sn" "$JAVA_HOME"
Both commands should report major version 25. Maven, Gradle, containers, and IDEs can select a different JDK from the shell, so verify the runtime inside the exact environment that builds or launches the application.
Build a JSON REST API with Java 25 and Spring Boot
Create a clean working directory and save the following example in the filename appropriate to its public class or build tool. XML, Gradle, Docker, and Kubernetes examples should be saved as pom.xml, build.gradle.kts, Dockerfile, or a YAML manifest respectively.
package com.example.demo;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
@RestController @RequestMapping("/api/messages")
class MessageController {
record Request(@NotBlank String text) {}
record Response(String text) {}
@PostMapping ResponseEntity create(@Valid @RequestBody Request request) {
return ResponseEntity.status(HttpStatus.CREATED).body(new Response(request.text().trim()));
}
}
Read the example before executing it. Replace demonstration hostnames, image names, identifiers, paths, and credentials with values from your environment. Never paste production secrets into source code or shell history.
How the Example Works
Keep controllers focused on HTTP, validate request DTOs, put business rules in services, and never expose persistence entities or exception traces directly.
Keep boundaries explicit: parse and validate external values before constructing domain objects; keep business rules separate from files, HTTP, databases, and framework code; propagate useful error context without exposing secrets. Prefer immutable values and narrow interfaces where they make ownership and change easier to understand.
Java types prevent many accidental states, but they do not validate untrusted input automatically. HTTP fields, environment variables, JSON values, database columns, and command-line arguments still require range, format, size, and authorization checks.
Run and Verify the Result
For a single source file, use source-file mode or compile explicitly:
java Main.java
javac Main.java
java Main
Replace Main.java with the example filename. For Maven use ./mvnw clean verify; for Gradle use ./gradlew clean test. For infrastructure examples, validate before applying: docker build, docker compose config, or kubectl apply --dry-run=server.
Verification must check behavior, not only exit status. Exercise valid, empty, boundary, malformed, unavailable-dependency, timeout, and unauthorized cases. Inspect logs, HTTP status and headers, generated artifacts, persistent state, resource use, and clean shutdown.
Common Errors and Fixes
UnsupportedClassVersionError
The runtime is older than the compiler target. Compare java --version and javac --version in the failing environment. Select matching alternatives or compile for a deliberate older target with --release.
ClassNotFoundException or NoClassDefFoundError
The classpath or packaged artifact is incomplete. Use the build wrapper, inspect the dependency tree, and confirm the runtime artifact contains the expected classes. Do not copy random JAR files into a server.
The command works locally but fails in a service or container
Compare working directory, user, environment, filesystem permissions, DNS, network policy, trust store, CPU and memory limits, and the actual JDK. Interactive shell variables are not automatically inherited by systemd, CI, or Kubernetes.
Topic-specific failure
Keep controllers focused on HTTP, validate request DTOs, put business rules in services, and never expose persistence entities or exception traces directly.
Production and Security Best Practices
- Use a supported JDK distribution and apply tested security updates.
- Pin and audit dependencies; build from a reviewed lockfile or resolved dependency definition.
- Run as a non-root account with the minimum filesystem and network access.
- Set connect, read, request, and shutdown timeouts explicitly.
- Keep secrets outside code, images, logs, and command arguments.
- Use structured logs, metrics, traces, health checks, and actionable alerts.
- Size heap and concurrency from measurements while leaving native-memory headroom.
- Test startup, graceful shutdown, dependency failure, restore, rollout, and rollback.
Frequently Asked Questions
Does this require Java 25?
The guide targets Java 25. Some examples work on earlier releases, but language features, library APIs, plugins, frameworks, and support policies vary.
Should I use java or javac?
javac compiles source into class files. java launches compiled classes, JAR applications, modules, or a single source file in source-file mode.
Should I use Maven or Gradle?
Either is suitable when configured reproducibly. Follow the existing project unless there is a measured reason to migrate, and always use the project wrapper in CI.
How should I troubleshoot production failures?
Start with the first error, deployment revision, runtime version, logs, health status, recent configuration changes, dependency reachability, and resource saturation. Preserve evidence before restarting repeatedly.
How do I make the example production-ready?
Add automated tests, input validation, secure configuration, dependency auditing, observability, resource controls, deployment checks, backup where state exists, and a tested rollback.
Conclusion
You now have a practical foundation for Spring Boot REST API. Keep the example small while learning, then make dependencies, failure behavior, security boundaries, and operational checks explicit before production.