Skip to main content

Building Your First REST API with Spring Boot - A Complete Guide

ยท 7 min read
Shutiye Dev
Software Engineer & Educator

Spring Boot has revolutionized Java backend development by simplifying the setup and configuration process. In this guide, we'll build a complete REST API for a simple task management system, covering all the essential concepts you need to get started.

What is Spring Boot?โ€‹

Spring Boot is an opinionated framework built on top of the Spring Framework. It eliminates much of the boilerplate configuration that traditional Spring applications require, allowing you to focus on writing business logic rather than XML configuration files.

Key Benefitsโ€‹

  • Auto-configuration: Automatically configures your application based on the dependencies you add
  • Embedded server: No need to deploy WAR files to external servers
  • Production-ready features: Built-in health checks, metrics, and monitoring
  • Minimal configuration: Convention over configuration approach

Prerequisitesโ€‹

Before we begin, make sure you have:

  • Java Development Kit (JDK) 17 or higher
  • Maven or Gradle (we'll use Maven)
  • Your favorite IDE (IntelliJ IDEA, Eclipse, or VS Code)
  • Basic understanding of Java and object-oriented programming

Project Setupโ€‹

Let's create a new Spring Boot project. The easiest way is using Spring Initializr:

  1. Visit start.spring.io
  2. Choose Maven as the build tool
  3. Select Java 17 or higher
  4. Add the following dependencies:
    • Spring Web
    • Spring Data JPA
    • H2 Database
    • Lombok (optional, but helpful)

Alternatively, if you prefer command-line:

curl https://start.spring.io/starter.zip \
-d dependencies=web,data-jpa,h2,lombok \
-d type=maven-project \
-d javaVersion=17 \
-d bootVersion=3.2.0 \
-o task-api.zip
unzip task-api.zip

Understanding the Project Structureโ€‹

After extracting the project, you'll see this structure:

task-api/
โ”œโ”€โ”€ src/
โ”‚ โ”œโ”€โ”€ main/
โ”‚ โ”‚ โ”œโ”€โ”€ java/
โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ com/example/taskapi/
โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ TaskApiApplication.java
โ”‚ โ”‚ โ””โ”€โ”€ resources/
โ”‚ โ”‚ โ””โ”€โ”€ application.properties
โ”‚ โ””โ”€โ”€ test/
โ”œโ”€โ”€ pom.xml
โ””โ”€โ”€ README.md

The TaskApiApplication.java is your entry point:

@SpringBootApplication
public class TaskApiApplication {
public static void main(String[] args) {
SpringApplication.run(TaskApiApplication.class, args);
}
}

The @SpringBootApplication annotation is a combination of three annotations:

  • @Configuration: Marks this class as a source of bean definitions
  • @EnableAutoConfiguration: Enables Spring Boot's auto-configuration
  • @ComponentScan: Scans for components in this package and sub-packages

Creating the Domain Modelโ€‹

Let's create our Task entity. This represents a task in our system:

package com.example.taskapi.model;

import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDateTime;

@Entity
@Data
public class Task {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false)
private String title;

private String description;

@Column(nullable = false)
private boolean completed = false;

private LocalDateTime createdAt;

private LocalDateTime updatedAt;

@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}

@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}

Key Points:

  • @Entity: Marks this class as a JPA entity (database table)
  • @Id: Marks the primary key field
  • @GeneratedValue: Auto-generates ID values
  • @PrePersist and @PreUpdate: Lifecycle callbacks for timestamps
  • @Data: Lombok annotation that generates getters, setters, and other methods

Creating the Repository Layerโ€‹

Spring Data JPA makes database operations incredibly simple. Create a repository interface:

package com.example.taskapi.repository;

import com.example.taskapi.model.Task;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;

@Repository
public interface TaskRepository extends JpaRepository<Task, Long> {
List<Task> findByCompleted(boolean completed);
List<Task> findByTitleContainingIgnoreCase(String title);
}

By extending JpaRepository, you automatically get these methods:

  • save(): Create or update a task
  • findById(): Find task by ID
  • findAll(): Get all tasks
  • deleteById(): Delete a task
  • And many more!

The custom methods (findByCompleted, findByTitleContainingIgnoreCase) are automatically implemented by Spring Data JPA based on their names.

Creating the Service Layerโ€‹

The service layer contains your business logic:

package com.example.taskapi.service;

import com.example.taskapi.model.Task;
import com.example.taskapi.repository.TaskRepository;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;

@Service
public class TaskService {
private final TaskRepository taskRepository;

public TaskService(TaskRepository taskRepository) {
this.taskRepository = taskRepository;
}

public List<Task> getAllTasks() {
return taskRepository.findAll();
}

public Optional<Task> getTaskById(Long id) {
return taskRepository.findById(id);
}

public Task createTask(Task task) {
return taskRepository.save(task);
}

public Task updateTask(Long id, Task taskDetails) {
Task task = taskRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Task not found"));

task.setTitle(taskDetails.getTitle());
task.setDescription(taskDetails.getDescription());
task.setCompleted(taskDetails.isCompleted());

return taskRepository.save(task);
}

public void deleteTask(Long id) {
taskRepository.deleteById(id);
}

public List<Task> getCompletedTasks() {
return taskRepository.findByCompleted(true);
}
}

Why use a service layer?

  • Separates business logic from controller logic
  • Makes code more testable
  • Allows transaction management
  • Provides a clear API for your application

Creating the REST Controllerโ€‹

Now let's expose our service through REST endpoints:

package com.example.taskapi.controller;

import com.example.taskapi.model.Task;
import com.example.taskapi.service.TaskService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
@RequestMapping("/api/tasks")
public class TaskController {
private final TaskService taskService;

public TaskController(TaskService taskService) {
this.taskService = taskService;
}

@GetMapping
public List<Task> getAllTasks() {
return taskService.getAllTasks();
}

@GetMapping("/{id}")
public ResponseEntity<Task> getTaskById(@PathVariable Long id) {
return taskService.getTaskById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}

@PostMapping
public ResponseEntity<Task> createTask(@RequestBody Task task) {
Task createdTask = taskService.createTask(task);
return ResponseEntity.status(HttpStatus.CREATED).body(createdTask);
}

@PutMapping("/{id}")
public ResponseEntity<Task> updateTask(
@PathVariable Long id,
@RequestBody Task task) {
try {
Task updatedTask = taskService.updateTask(id, task);
return ResponseEntity.ok(updatedTask);
} catch (RuntimeException e) {
return ResponseEntity.notFound().build();
}
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteTask(@PathVariable Long id) {
taskService.deleteTask(id);
return ResponseEntity.noContent().build();
}

@GetMapping("/completed")
public List<Task> getCompletedTasks() {
return taskService.getCompletedTasks();
}
}

Understanding the annotations:

  • @RestController: Combines @Controller and @ResponseBody
  • @RequestMapping: Maps HTTP requests to handler methods
  • @GetMapping, @PostMapping, etc.: Shortcuts for specific HTTP methods
  • @PathVariable: Extracts values from the URL path
  • @RequestBody: Converts JSON request body to Java object

Configuring the Databaseโ€‹

Add these properties to application.properties:

# H2 Database Configuration
spring.datasource.url=jdbc:h2:mem:taskdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

# JPA Configuration
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true

# H2 Console (for development)
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

Running the Applicationโ€‹

Start your application:

mvn spring-boot:run

Your API will be available at http://localhost:8080/api/tasks

Testing the APIโ€‹

Use curl, Postman, or any HTTP client:

Create a task:

curl -X POST http://localhost:8080/api/tasks \
-H "Content-Type: application/json" \
-d '{
"title": "Learn Spring Boot",
"description": "Complete the tutorial",
"completed": false
}'

Get all tasks:

curl http://localhost:8080/api/tasks

Get a specific task:

curl http://localhost:8080/api/tasks/1

Update a task:

curl -X PUT http://localhost:8080/api/tasks/1 \
-H "Content-Type: application/json" \
-d '{
"title": "Learn Spring Boot",
"description": "Complete the tutorial",
"completed": true
}'

Delete a task:

curl -X DELETE http://localhost:8080/api/tasks/1

Best Practicesโ€‹

1. Use DTOs (Data Transfer Objects)โ€‹

Instead of exposing your entity directly, create DTOs:

public class TaskDTO {
private Long id;
private String title;
private String description;
private boolean completed;
// getters and setters
}

2. Implement Proper Exception Handlingโ€‹

Create a global exception handler:

@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(
ResourceNotFoundException ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
ex.getMessage()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
}

3. Add Validationโ€‹

Use Bean Validation annotations:

public class Task {
@NotBlank(message = "Title is required")
@Size(min = 3, max = 100)
private String title;

@Size(max = 500)
private String description;
}

4. Use Proper HTTP Status Codesโ€‹

  • 200 OK: Successful GET, PUT
  • 201 Created: Successful POST
  • 204 No Content: Successful DELETE
  • 400 Bad Request: Validation errors
  • 404 Not Found: Resource doesn't exist
  • 500 Internal Server Error: Server errors

What's Next?โ€‹

Now that you understand the basics, explore these advanced topics:

  • Security: Add Spring Security for authentication and authorization
  • Testing: Write unit and integration tests
  • Documentation: Use Swagger/OpenAPI for API documentation
  • Caching: Implement caching with Redis
  • Pagination: Add pagination for large datasets
  • Database: Switch from H2 to PostgreSQL or MySQL

Conclusionโ€‹

Spring Boot makes it incredibly easy to build production-ready REST APIs. With minimal configuration, you can create a fully functional backend that follows best practices and industry standards.

The key takeaways:

  • Spring Boot eliminates boilerplate configuration
  • Layered architecture (Controller โ†’ Service โ†’ Repository) keeps code organized
  • JPA repositories handle database operations with minimal code
  • REST controllers make creating APIs straightforward

Start building your own REST APIs with Spring Boot today!


Have questions or want to see more Spring Boot tutorials? Connect with us on Twitter or check out our curriculum.