Spring Boot Hello World Part 1

post-thumb

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

LayerComponentResponsibilityFiles / PackagesNotes
ClientCLI (curl) / Postman / HTTPieSend HTTP requests to test the APIUse curl in examples; GUI tools like Postman also work.
API (Web)Spring Boot MVC (@RestController)Expose REST endpoints under /taskscontrollers/TaskController.javaProduces JSON (produces = "application/json").
DTOsRequest/Response classesDefine request/response shapes for each endpointcontrollers/requests_and_responses/*Keeps payloads explicit and type-safe.
Domain ModelTaskRepresents a task entity (id, name)models/Task.javaUses UUID for IDs.
In-Memory StoreList<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 HandlingResponseStatusExceptionReturn proper HTTP status codes (e.g., 404 when not found)Thrown in controller methodsAvoids returning null; clearer API behavior.
Build ToolingGradle + Dependency ManagementCompile, package, manage dependenciesbuild.gradleUses Spring Boot plugin and BOM (io.spring.dependency-management).
RuntimeSpring Boot Auto-ConfigurationBootstraps the app, configures embedded serverApplication.java (@SpringBootApplication)Run with java -jar ....
Java PlatformJava 21Language/runtime versionConfigured 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.

Application started

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:

MethodEndpointDescription
GET/tasksGet all tasks
GET/tasks/{id}Get single task
POST/tasksCreate 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"}}
comments powered by Disqus