Spring Boot Hello World Part 1
Java is still one of the most widely used programming languages in the world, and Spring Boot is the de-facto framework for building production-ready services with it. Surprisingly, even though I’ve worked with Java before, I had never actually touched Spring Boot.
That changes today.
In this series, we’ll build a simple CRUD API for tasks step by step.
By the end of this part, you’ll have a running Spring Boot application that you can query with curl or Postman — your first working “Hello World” REST service.
👉 Source for this part: Git repository (part 1 branch).
👉 Parts:
- Part 1 (this post): Basics — install the tools and build a simple REST API.
- Part 2: Add unit tests for our endpoints.
- Part 3: Store tasks in a real NoSQL database (Redis).
Architecture
Layer | Component | Responsibility | Files / Packages | Notes |
---|---|---|---|---|
Client | CLI (curl) / Postman / HTTPie | Send HTTP requests to test the API | — | Use curl in examples; GUI tools like Postman also work. |
API (Web) | Spring Boot MVC (@RestController ) | Expose REST endpoints under /tasks | controllers/TaskController.java | Produces JSON (produces = "application/json" ). |
DTOs | Request/Response classes | Define request/response shapes for each endpoint | controllers/requests_and_responses/* | Keeps payloads explicit and type-safe. |
Domain Model | Task | Represents a task entity (id, name) | models/Task.java | Uses UUID for IDs. |
In-Memory Store | List<Task> | Temporary data store for tasks (no persistence yet) | controllers/TaskController.java (field private List<Task> tasks ) | Data is lost on restart; Part 3 moves this to Redis. |
Error Handling | ResponseStatusException | Return proper HTTP status codes (e.g., 404 when not found) | Thrown in controller methods | Avoids returning null ; clearer API behavior. |
Build Tooling | Gradle + Dependency Management | Compile, package, manage dependencies | build.gradle | Uses Spring Boot plugin and BOM (io.spring.dependency-management ). |
Runtime | Spring Boot Auto-Configuration | Bootstraps the app, configures embedded server | Application.java (@SpringBootApplication ) | Run with java -jar ... . |
Java Platform | Java 21 | Language/runtime version | Configured in Gradle (sourceCompatibility / targetCompatibility or java {} block) | Stick to LTS for stability. |
Prerequisites
You’ll need any Linux environment. I’m using Windows Subsystem for Linux (WSL2) with Ubuntu 24.04 “Noble”.
Basic familiarity with the terminal will help, but no prior Spring Boot experience is required.
Installing the Tools
We’ll use SDKMAN to install Java, Gradle, and Spring Boot. SDKMAN makes it easy to switch between versions without messing with system packages.
First, install dependencies (on Ubuntu):
sudo apt install zip curl
Then install SDKMAN and the required tools:
# sdkman installer needs zip, at least in WSL Ubuntu it is not installed by default. Curl is needed later to test endpoints.
sudo apt install zip curl
# Install SDKMAN
curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
# Install Java, Gradle, and Spring Boot
sdk install java
sdk install gradle
sdk install springboot
That’s it, now we’re ready to create our application.
Project Setup
We’ll build a simple REST API with CRUD operations on a Task model. In this first part, tasks will just be stored in memory.
Configure Gradle and the Application Main
Create build.gradle with the following:
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.5'
}
group = 'fi.janihast'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '21'
targetCompatibility = '21'
}
repositories {
mavenCentral()
}
dependencies {
implementation(platform("org.springframework.boot:spring-boot-dependencies:3.5.5"))
implementation 'org.springframework.boot:spring-boot-starter-web'
}
Verify everything works:
gradle classes
gradle dependencies
Now add the application main class in src/main/java/fi/janihast/springboot_hello_world/Application.java
:
package fi.janihast.springboot_hello_world;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Build and run:
gradle bootJar
java -jar build/libs/spring-boot-hello-world-0.0.1-SNAPSHOT.jar
Spring Boot starts up, but since we haven’t defined any endpoints yet, there’s nothing to call. Stop the application with Ctrl+C.

Add the Task Model and DTOs
We’ll create a Task model with an id (UUID) and name. Later we’ll use Data Transfer Objects (DTOs) for requests and responses — a common practice to keep APIs clean and type-safe. Normally, DTOs should be kept separate from domain models, but here we’ll keep things simple and reuse the Task model inside responses.
Example model: src/main/java/fi/janihast/springboot_hello_world/models/Task.java
package fi.janihast.springboot_hello_world.models;
import java.util.UUID;
public class Task {
private UUID id;
private String name;
// Setter and getter for id
public UUID getID() {
return this.id;
}
public void setID(UUID id) {
this.id = id;
}
// Setter and getter for name
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
We also define simple request/response classes like TaskPostRequest, TaskPostResponse, TaskGetAllResponse, etc for DTO usage. Each just wraps the Task or a list of tasks. (See the repo for the full list — the pattern is the same for POST/PUT/GET/DELETE.)
Add Controller
Create src/main/java/fi/janihast/springboot_hello_world/controllers/TaskController.java
:
package fi.janihast.springboot_hello_world.controllers;
import fi.janihast.springboot_hello_world.models.Task;
import java.util.ArrayList;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(value = "/tasks", produces = "application/json")
public class TaskController {
private List<Task> tasks = new ArrayList<>();
}
This sets up a REST controller where all endpoints live under /tasks and return JSON. And then we can add endpoint one by one.
Create Task
@PostMapping(value = "", consumes = "application/json")
public TaskPostResponse postTask(@RequestBody TaskPostRequest request) {
UUID uuid = UUID.randomUUID();
Task task = new Task();
task.setID(uuid);
task.setName(request.getName());
this.tasks.add(task);
TaskPostResponse response = new TaskPostResponse();
response.setTask(task);
return response;
}
Get All Tasks
@GetMapping(value = "")
public TaskGetAllResponse getTasks() {
TaskGetAllResponse response = new TaskGetAllResponse();
response.setTasks(this.tasks);
return response;
}
Get Single task
@GetMapping(value = "/{taskID}")
public TaskGetResponse getTask(@PathVariable UUID taskID) {
TaskGetResponse response = new TaskGetResponse();
for (Task task : this.tasks) {
if (task.getID().equals(taskID)) {
response.setTask(task);
return response;
}
}
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Task not found");
}
Update Task
@PutMapping(value = "/{taskID}")
public TaskPutResponse putTask(@PathVariable UUID taskID, @RequestBody TaskPutRequest request) {
TaskPutResponse response = new TaskPutResponse();
for (Task task : this.tasks) {
if (task.getID().equals(taskID)) {
task.setName(request.getName());
response.setTask(task);
return response;
}
}
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Task not found");
}
Delete Task
@DeleteMapping(value = "/{taskID}")
public TaskDeleteResponse deleteTask(@PathVariable UUID taskID) {
TaskDeleteResponse response = new TaskDeleteResponse();
for (Task task : this.tasks) {
if (task.getID().equals(taskID)) {
this.tasks.remove(task);
response.setTask(task);
return response;
}
}
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Task not found");
}
Recap
At this point, we have a fully working CRUD API:
Method | Endpoint | Description |
---|---|---|
GET | /tasks | Get all tasks |
GET | /tasks/{id} | Get single task |
POST | /tasks | Create new task |
PUT | /tasks/{id} | Update existing task |
DELETE | /tasks/{id} | Delete existing task |
The tasks are only stored in memory, so restarting the app clears them — but it’s enough for a working prototype.
In the next part, we’ll add unit tests to validate our endpoints automatically.
References
Quick Reference
Start the Application
gradle bootJar
java -jar build/libs/spring-boot-hello-world-0.0.1-SNAPSHOT.jar
Create New Task
curl -H "Content-Type: application/json" \
-X POST -d '{"name":"my first task"}' \
http://localhost:8080/tasks
Response
{"task":{"id":"<uuid>","name":"my first task"}}
Get All Existing Tasks
curl http://localhost:8080/tasks
Response
{"tasks":[{"id":"<uuid>","name":"my first task"}]}
Get Single Existing Task
curl http://localhost:8080/tasks/<uuid>
# Replace <uuid> with a real UUID returned from Create
Response
{"task":{"id":"<uuid>","name":"my first task"}}
Update Existing Task
curl -H "Content-Type: application/json" \
-X PUT -d '{"name":"updated task"}' \
http://localhost:8080/tasks/<uuid>
# Replace <uuid> with a real UUID returned from Create
Response
{"task":{"id":"<uuid>","name":"updated task"}}
Delete Existing Task
curl -X DELETE http://localhost:8080/tasks/<uuid>
# Replace <uuid> with a real UUID returned from Create
Response
{"task":{"id":"<uuid>","name":"updated task"}}