How to Build a REST API with Spring Boot: A Step-by-Step Guide

From setting up your project to securing your endpoints, this guide lays the foundation for your API.
  • Blog
  • >
  • How to Build a REST API with Spring Boot: A Step-by-Step Guide

REST APIs are everywhere in today’s web and mobile applications. They play a crucial role in allowing different systems and services to communicate smoothly, making it easier to share data and functionality across platforms. It’s gotten to the point that knowing how to create a good RESTful API is a valuable skill.

Spring Boot stands out as one of the top choices for building RESTful services in Java. It’s favored by developers for its straightforward setup and how quickly you can get your API up and running. With Spring Boot, you don’t have to deal with complex configurations, allowing you to focus more on developing your application’s features. Additionally, it taps into the vast Java ecosystem, offering a wide range of libraries and tools that make adding functionalities like security, data management, and scalability much easier.

In this guide, we’ll take you through how Spring Boot can simplify and enhance the process, providing a more powerful and efficient way to build your REST API.

Note: If you’re just getting started with REST APIs in Java and want to learn the basics without using Spring Boot, check out our basic guide on creating a RESTful API in Java

1. Setting up your Spring Boot project

Getting started with Spring Boot is straightforward, thanks to the tools and resources available. In this section, we’ll walk you through setting up your Spring Boot project, covering both the Spring Initializr method and alternative approaches using Maven or Gradle.

Create a new Spring Boot project

Spring Initializr is a web-based tool that simplifies the process of generating a Spring Boot project with the necessary dependencies. Here’s how to use it:

  1. Navigate to Spring Initializr at start.spring.io.
  2. Configure your project:
    • Project: Choose either Maven or Gradle as your build tool.
    • Language: Select Java.
    • Spring Boot: Choose the latest stable version.
    • Project metadata: Fill in details like Group (e.g., com.example) and Artifact (e.g., demo).
  3. Select dependencies:
    • Spring Web: Provides REST support.
    • Spring Boot DevTools: Enables hot reloading for faster development.
  4. Click Generate to download a ZIP file containing your new project.
  5. Extract the ZIP file and open the project in your preferred IDE (e.g., IntelliJ IDEA, Eclipse).

Alternative methods: Using Maven or Gradle

If you prefer setting up your project using the command line with Maven or Gradle, follow the instructions below.

Using Maven:

Open your terminal and run the following command to generate a Spring Boot project with Maven:

mvn archetype:generate -DgroupId=com.example -DartifactId=demo -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false

After generating the project, add the necessary Spring Boot dependencies to your pom.xml file.

Using Gradle:

For Gradle users, execute the following command in your terminal:

gradle init --type java-application

Once the project is initialized, add the Spring Boot plugins and dependencies to your build.gradle file.

Code example: Generating a project with Spring Initializr

Here’s a quick example of how your pom.xml might look after selecting the necessary dependencies using Spring Initializr:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
    </dependency>
    <!-- Add other dependencies as needed -->
</dependencies>

Project structure overview

Once you’ve set up your project, it’s helpful to understand its basic structure. Here’s a breakdown of the main components we’ll be working with:

  • src/main/java: This folder contains all your application’s source code. You’ll typically organize your code into packages, such as controller, service, and repository, to maintain a clean structure.
  • src/main/resources: This directory holds configuration files and other resources. Key files include:
    • application.properties or application.yml: Configuration settings for your Spring Boot application.
    • Static resources: If you’re serving static content, such as HTML, CSS, or JavaScript files, they go here.
  • Application.java: Located in the src/main/java directory, this class serves as the entry point for your Spring Boot application. It contains the main method that bootstraps the application. Here’s what it typically looks like: 
package com.example.demo;

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);
    }
}

In this file, we have the following annotations:

  • @SpringBootApplication: This annotation marks the main class of a Spring Boot application. It combines three other annotations:
    • @EnableAutoConfiguration: Tells Spring Boot to start adding beans based on classpath settings, other beans, and various property settings.
    • @ComponentScan: Enables component scanning so that the web controller classes and other components you create will be automatically discovered and registered as beans in the Spring application context.
    • @Configuration: Allows you to register extra beans in the context or import additional configuration classes.

2. Creating your first REST API endpoint

Now that your Spring Boot project is set up, it’s time to create your first REST API endpoint. This involves building a Controller that will handle HTTP requests and send responses back to the client.

Build the controller

In Spring Boot, a controller is a crucial component that manages incoming HTTP requests and returns appropriate responses. It acts as the intermediary between the client and your application’s logic, processing requests and sending back data in formats like JSON or XML.

This also means that usually, your business logic is not inside the control but rather somewhere else, and the controller is just interacting with it. Keep this in mind when you build your own APIs—sometimes developers forget this part and couple the actual business logic with the API’s controller code, creating a complicated mess in their code.

With that out of the way, here’s how to create a simple controller in Spring Boot.

Let’s create a WelcomeController that responds to a GET request with a welcome message.

In your project’s src/main/java/com/example/demo directory (adjust the package name as necessary), create a new Java class named WelcomeController.java.

package com.example.demo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class WelcomeController {

    @GetMapping("/welcome")
    public String welcome() {
        return "Welcome to the Spring Boot REST API!";
    }
}

Here’s what’s happening with this code:

  • @RestController: This annotation marks the class as a Controller where every method returns a domain object instead of a view. It’s a convenience annotation that combines @Controller and @ResponseBody.
  • @GetMapping("/welcome"): This annotation maps HTTP GET requests to the welcome() method. When a client sends a GET request to /welcome, this method is invoked.
  • public String welcome(): This method returns a simple welcome message as a plain text response. Spring Boot automatically converts this to the appropriate HTTP response.

Run the application

Start your Spring Boot application by running the Application class. You can do this from your IDE by right-clicking the Application class and selecting Run or by using the command line: 

./mvnw spring-boot:run

Or if you’re using Gradle:

./gradlew bootRun

Test the endpoint

Once the application is running, you can test your new endpoint. Open your web browser or use a tool like curl or Postman to send a GET request to http://localhost:8080/welcome. You should receive the following response:

Welcome to the Spring Boot REST API!

This simple example demonstrates how to create a REST API endpoint using Spring Boot. The WelcomeController handles HTTP GET requests to the /welcome path and returns a welcome message.

3. Adding data models and business logic

With your first REST API endpoint up and running, it’s time to expand your application by introducing data models and business logic. This section will guide you through defining your data structures and creating a service layer to manage your application’s core functionality.

Define the data model

In any application, it’s essential to have a clear representation of the data you’ll be working with (i.e., your users, your products, the shopping cart, etc). In Java, Plain Old Java Objects (POJOs) provide a simple and effective way to model your data.

POJO is just a fancy name for regular Java classes without any special restrictions or requirements, making them easy to create and maintain.

Create a User class that represents a user in your application. This class will have fields for id, name, and email.

In your project’s src/main/java/com/example/demo/model directory (you may need to create the model folder), create a new Java class named User.java. Add the following code:

package com.example.demo.model;
import jakarta.persistence.Entity;
import jakarta.persistence.*;


@Entity
@Table(name="my_users")
public class User {
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String email;

    // Default constructor
    public User() {
    }

    // Parameterized constructor
    public User(Long id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }

    // Getters and Setters
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    // toString method
    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", email='" + email + '\'' +
                '}';
    }
}

Here’s what’s happening in this file:

  • Fields: The User class has three private fields: id (of type Long), name, and email (both of type String).
  • Constructors:
    • Default constructor: Allows for the creation of a User object without setting any fields initially.
    • Parameterized constructor: Enables the creation of a User object with all fields initialized.
  • Getters and setters: Provide access to the private fields, allowing other parts of the application to retrieve and modify their values.
  • toString method: Offers a readable string representation of the User object, which can be useful for debugging and logging.

This simple POJO serves as the foundation for your data model, representing the structure of the user data your API will handle.

Create the service layer

Separating business logic from your controllers is a best practice in software development (remember, we gotta stay away from spaghetti code!). The service layer handles the core functionality of your application, such as processing data and applying business rules, while the controller layer manages HTTP requests and responses. This separation enhances the maintainability and scalability of your application.

Let’s create a UserService that manages a list of users. For this example, we’ll use an in-memory list to store user data.

In your project’s src/main/java/com/example/demo/service directory (create the service package if it doesn’t exist), create a new Java class named UserService.java. Add the following code:

package com.example.demo.service;

import com.example.demo.model.User;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class UserService {
    private List<User> users = new ArrayList<>();

    // Constructor to initialize with some users
    public UserService() {
        users.add(new User(1L, "John Doe", "john.doe@example.com"));
        users.add(new User(2L, "Jane Smith", "jane.smith@example.com"));
    }

    // Method to retrieve all users
    public List<User> getAllUsers() {
        return users;
    }

    // Method to add a new user
    public void addUser(User user) {
        users.add(user);
    }

    // Method to find a user by ID
    public User getUserById(Long id) {
        return users.stream()
                .filter(user -> user.getId().equals(id))
                .findFirst()
                .orElse(null);
    }
}

And for this file, here’s what’s going on:

  • @Service annotation: Marks the UserService class as a Spring service component, making it eligible for component scanning and dependency injection.
  • User list: Maintains an in-memory list of User objects. In a real-world application, this would typically be replaced with a database.
  • Constructor: Initializes the service with a couple of sample users for demonstration purposes.
  • getAllUsers method: Returns the list of all users.
  • addUser method: Adds a new User to the list.
  • getUserById method: Searches for a User by their id and returns the user if found; otherwise, returns null.

Integrate the service with the controller

To use the UserService in your controller, inject it into your WelcomeController or create a new controller dedicated to user operations.

Here’s how you can modify the WelcomeController to include user-related endpoints:

package com.example.demo;

import com.example.demo.model.User;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class WelcomeController {

    @Autowired
    private UserService userService;

    @GetMapping("/welcome")
    public String welcome() {
        return "Welcome to the Spring Boot REST API!";
    }

    @GetMapping("/users")
    public List<User> getAllUsers() {
        return userService.getAllUsers();
    }

    @GetMapping("/users/{id}")
    public User getUserById(@PathVariable Long id) {
        return userService.getUserById(id);
    }

    @PostMapping("/users")
    public void addUser(@RequestBody User user) {
        userService.addUser(user);
    }
}

Let’s take a closer look at what’s happening with this code:

  • @Autowired annotation: Injects the UserService into the WelcomeController, allowing the controller to use the service’s methods.
  • New endpoints:
    • GET /users: Retrieves the list of all users by calling userService.getAllUsers().
    • GET /users/{id}: Retrieves a specific user by their id using userService.getUserById(id).
    • POST /users: Adds a new user to the list by calling userService.addUser(user). The @RequestBody annotation indicates that the user data will be sent in the request body in JSON format.

Test the service layer

Restart your Spring Boot application and test the new endpoints to ensure everything is working as expected.

curl http://localhost:8080/users

You should get a response that looks somewhat like this:

[
  {
    "id": 1,
    "name": "John Doe",
    "email": "john.doe@example.com"
  },
  {
    "id": 2,
    "name": "Jane Smith",
    "email": "jane.smith@example.com"
  }
]

Retrieve a user by ID

curl http://localhost:8080/users/1

This endpoint should return the JSON containing the data from user with ID 1:

{
  "id": 1,
  "name": "John Doe",
  "email": "john.doe@example.com"
}

Add a new user

curl -X POST -H "Content-Type: application/json" -d '{"id":3,"name":"Alice Johnson","email":"alice.johnson@example.com"}' http://localhost:8080/users

The response will be empty, as there is no body returned as part of the creation process. You should get a “No content (HTTP 200 OK)” response.

Verify the addition:

curl http://localhost:8080/users

The new user should now be returned as part of the list of users:

[
  {
    "id": 1,
    "name": "John Doe",
    "email": "john.doe@example.com"
  },
  {
    "id": 2,
    "name": "Jane Smith",
    "email": "jane.smith@example.com"
  },
  {
    "id": 3,
    "name": "Alice Johnson",
    "email": "alice.johnson@example.com"
  }
]

These tests confirm that your data model and service layer are functioning correctly, allowing you to manage user data through your REST API.

4. Connecting to a database

Now let’s take this example to the next level by incorporating an actual database for persistence.

Spring Boot makes the process of connecting to a database seamless by integrating with Spring Data JPA, which allows you to interact with databases using Java objects. In this section, we’ll set up database connectivity using an in-memory H2 database for simplicity and create a JPA repository to manage your data.

Setting up a database

Spring Data JPA provides a robust and flexible way to interact with relational databases. For development and testing purposes, using an in-memory database like H2 is useful because it requires minimal configuration and doesn’t persist data after the application stops (so, it’s “kind of a database,” but you get the point).

Add the Spring Data JPA and H2 dependencies

First, include the necessary dependencies in your project to enable Spring Data JPA and the H2 database.

Using Maven:

Open your pom.xml file and add the following dependencies within the <dependencies> section:

<dependencies>
    <!-- Existing dependencies -->

    <!-- Spring Data JPA -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <!-- H2 Database -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>

    <!-- Other dependencies as needed -->
</dependencies>

Using Gradle:

Open your build.gradle file and add the following dependencies:

dependencies {
    // Existing dependencies

    // Spring Data JPA
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

    // H2 Database
    runtimeOnly 'com.h2database:h2'

    // Other dependencies as needed
}

Configure the H2 Database

Next, configure Spring Boot to use the H2 in-memory database. Open the src/main/resources/application.properties file and add the following configurations:

# H2 Database Configuration
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

# Enable H2 Console
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

# Show SQL Statements in the Console (Optional)
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update

Here’s the explanation of the configuration from above:

  • spring.datasource.url: Specifies the JDBC URL for the H2 in-memory database named testdb.
  • spring.datasource.driverClassName: The driver class for H2.
  • spring.datasource.username & spring.datasource.password: Credentials for accessing the database. The default username for H2 is sa with an empty password.
  • spring.jpa.database-platform: Specifies the Hibernate dialect for H2.
  • spring.h2.console.enabled: Enables the H2 database console for easy access.
  • spring.h2.console.path: Sets the path to access the H2 console at http://localhost:8080/h2-console.
  • spring.jpa.show-sql: (Optional) Enables logging of SQL statements in the console.
  • spring.jpa.hibernate.ddl-auto: Automatically creates and updates the database schema based on your JPA entities.

Create a JPA repository

Spring Data JPA simplifies data access by providing repository interfaces that handle common CRUD operations. By extending Spring Data JPA’s JpaRepository interface, you can interact with the database without writing boilerplate code.

Create the repository interface

To manage User entities in the database, create a repository interface that extends JpaRepository. This interface provides various methods for performing CRUD operations.

In your project’s src/main/java/com/example/demo directory, create a new package named repository.

Inside the repository package, create a new Java interface named UserRepository.java with the following content: 

package com.example.demo.repository;

import com.example.demo.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    // Additional query methods can be defined here if needed
}

Here’s what’s happening with the above code:

  • @Repository annotation: Although not strictly necessary since Spring Data JPA automatically detects repository interfaces, adding @Repository enhances clarity and allows for exception translation.
  • Extending JpaRepository: By extending JpaRepository<User, Long>, the UserRepository interface inherits several methods for working with User persistence, including methods for saving, deleting, and finding User entities.
  • Generic parameters:
    • User: The type of the entity to manage.
    • Long: The type of the entity’s primary key.

Update the service layer to use the repository

With the repository in place, update your UserService to interact with the database instead of using an in-memory list.

Modify the UserService Class. Open UserService.java in the service package and update it as follows:

package com.example.demo.service;

import com.example.demo.model.User;
import com.example.demo.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service
public class UserService {

    private final UserRepository userRepository;

    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    // Method to retrieve all users
    public List<User> getAllUsers() {
        return userRepository.findAll();
    }

    // Method to add a new user
    public User addUser(User user) {
        return userRepository.save(user);
    }

    // Method to find a user by ID
    public Optional<User> getUserById(Long id) {
        return userRepository.findById(id);
    }

    // Method to update a user
    public User updateUser(Long id, User userDetails) {
        User user = userRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("User not found with id " + id));
        user.setName(userDetails.getName());
        user.setEmail(userDetails.getEmail());
        return userRepository.save(user);
    }

    // Method to delete a user
    public void deleteUser(Long id) {
        userRepository.deleteById(id);
    }
}

Here’s the detailed explanation of what’s happening with the updated code:

  • Dependency injection: The UserRepository is injected into the UserService using constructor injection, promoting immutability, and easier testing.
  • CRUD methods: The service methods now delegate data operations to the UserRepository, utilizing methods like findAll(), save(), and findById() provided by JpaRepository.
  • Optional return type: The getUserById method returns an Optional<User>, which helps handle cases where a user with the specified ID might not exist.
  • Additional methods: Methods for updating and deleting users have been added to demonstrate more comprehensive CRUD operations.

Update the controller to use the updated service

Finally, update your controller to use the updated UserService methods that interact with the database.

To modify the WelcomeController class, open WelcomeController.java and update it as follows:

package com.example.demo;

import com.example.demo.model.User;
import com.example.demo.service.ResourceNotFoundException;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api")
public class WelcomeController {

    private final UserService userService;

    @Autowired
    public WelcomeController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/welcome")
    public String welcome() {
        return "Welcome to the Spring Boot REST API!";
    }

    @GetMapping("/users")
    public List<User> getAllUsers() {
        return userService.getAllUsers();
    }

    @GetMapping("/users/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        User user = userService.getUserById(id)
                .orElseThrow(() -> new ResourceNotFoundException("User not found with id " + id));
        return ResponseEntity.ok(user);
    }

    @PostMapping("/users")
    public User addUser(@RequestBody User user) {
        return userService.addUser(user);
    }

    @PutMapping("/users/{id}")
    public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User userDetails) {
        User updatedUser = userService.updateUser(id, userDetails);
        return ResponseEntity.ok(updatedUser);
    }

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

This is what’s happening:

  • @RequestMapping(“/api”): Sets a base path for all endpoints in the controller, e.g., /api/welcome and /api/users.
  • CRUD endpoints:
    • GET /api/users: Retrieves all users.
    • GET /api/users/{id}: Retrieves a user by ID. Throws ResourceNotFoundException if the user doesn’t exist.
    • POST /api/users: Adds a new user.
    • PUT /api/users/{id}: Updates an existing user.
    • DELETE /api/users/{id}: Deletes a user by ID.
  • ResponseEntity: Provides more control over the HTTP response, allowing you to set status codes and headers as needed.

5. Handling HTTP methods: GET, POST, PUT, DELETE

With your Spring Boot project connected to a database and your data models in place, it’s time to implement the core functionalities of your REST API. This means handling the primary HTTP methods—GET, POST, PUT, and DELETE—to perform Create, Read, Update, and Delete (CRUD) operations on your User entities.

Implement CRUD operations with Spring Boot

CRUD operations are fundamental to any REST API, allowing clients to manage resources effectively. Here’s how you can implement each of these operations in your Spring Boot application.

GET Request: Retrieve a List of Users or a Specific User by ID

To fetch a list of all users, you’ll create a GET endpoint that returns a collection of User objects.

@GetMapping("/users")
public List<User> getAllUsers() {
    return userService.getAllUsers();
}

The code is quite straightforward, but here’s the relevant parts of it:

  • @GetMapping("/users"): Maps HTTP GET requests to /api/users to this method.
  • public List<User> getAllUsers(): Returns a list of all users by invoking the getAllUsers() method from the UserService.

To fetch a single user by their ID, create another GET endpoint that accepts a path variable.

@GetMapping("/users/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
    User user = userService.getUserById(id)
            .orElseThrow(() -> new ResourceNotFoundException("User not found with id " + id));
    return ResponseEntity.ok(user);
}

Here’s what’s happening:

  • @GetMapping("/users/{id}"): Maps HTTP GET requests to /api/users/{id} to this method.
  • @PathVariable Long id: Binds the {id} path variable to the id parameter.
  • userService.getUserById(id): Retrieves the user from the service layer.
  • ResponseEntity.ok(user): Returns the user with an HTTP 200 OK status.
  • Throws ResourceNotFoundException if the user is not found, which results in a 404 Not Found response.

POST request: add a new user

To add a new user to your database, create a POST endpoint that accepts user data in the request body.

@PostMapping("/users")
public User addUser(@RequestBody User user) {
    return userService.addUser(user);
}

Again, very straightforward, mainly thanks to JPA:

  • @PostMapping("/users"): Maps HTTP POST requests to /api/users to this method.
  • @RequestBody User user: Binds the incoming JSON payload to a User object.
  • userService.addUser(user): Saves the new user using the service layer.
  • Returns the saved User object, which includes the generated ID.

Example request body:

{
    "name": "Alice Johnson",
    "email": "alice.johnson@example.com"
}

PUT request: update an existing user

To update the details of an existing user, create a PUT endpoint that accepts the user ID and the updated data. This endpoint will update the changed properties to the element matching the ID provided.

@PutMapping("/users/{id}")
public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User userDetails) {
    User updatedUser = userService.updateUser(id, userDetails);
    return ResponseEntity.ok(updatedUser);
}

And here are the details of the new method:

  • @PutMapping("/users/{id}"): Maps HTTP PUT requests to /api/users/{id} to this method.
  • @PathVariable Long id: Binds the {id} path variable to the id parameter.
  • @RequestBody User userDetails: Binds the incoming JSON payload to a User object containing updated data.
  • userService.updateUser(id, userDetails): Updates the user using the service layer.
  • Returns the updated User object with an HTTP 200 OK status.

Example request body:

PUT /users/1
{
    "name": "Alice Smith",
    "email": "alice.smith@example.com"
}

 DELETE request: delete a user by ID

To remove a user from your database, create a DELETE endpoint that accepts the user ID.

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

This is a simple endpoint that has a few interesting points to notice:

  • @DeleteMapping("/users/{id}"): Maps HTTP DELETE requests to /api/users/{id} to this method.
  • @PathVariable Long id: Binds the {id} path variable to the id parameter.
  • userService.deleteUser(id): Deletes the user using the service layer.
  • Returns an HTTP 204 No Content status, indicating that the deletion was successful and there is no content to return.

ResourceNotFoundException class

To handle scenarios where a user is not found, create a custom exception class. This class helps in providing meaningful error responses to the client.

package com.example.demo.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}

And here’s what’s happening:

  • Package declaration: Place this class in an exception package within your project structure.
  • @ResponseStatus(HttpStatus.NOT_FOUND): Automatically sets the HTTP status to 404 Not Found when this exception is thrown.
  • Constructor: Accepts a custom error message that describes the exception.

6. Best practices for building REST APIs

Building a REST API is not just about making endpoints available; it’s also about ensuring that your API is reliable, maintainable, and secure. Adhering to best practices can significantly enhance the quality and usability of your API. In this section, we’ll cover some essential best practices to follow when building REST APIs with Spring Boot.

Follow REST principles

Adhering to REST (Representational State Transfer) principles ensures that your API is intuitive and easy to use. Key aspects include:

Use proper HTTP methods

Each HTTP method should correspond to a specific type of operation:

  • GET: Retrieve data from the server. Use for fetching resources without modifying them.
  • POST: Create a new resource on the server.
  • PUT: Update an existing resource entirely.
  • DELETE: Remove a resource from the server.

Use appropriate HTTP status codes

HTTP status codes communicate the result of an API request. Using the correct status codes helps clients understand the outcome of their requests.

  • 200 OK: The request was successful.
  • 201 Created: A new resource was successfully created.
  • 204 No Content: The request was successful, but there’s no content to return.
  • 400 Bad Request: The request was malformed or invalid.
  • 404 Not Found: The requested resource does not exist.
  • 500 Internal Server Error: An unexpected error occurred on the server.

Use versioning

API versioning helps ensure that changes or updates to your API don’t break existing clients. By versioning your API, you can introduce new features or make changes without disrupting service for users relying on older versions (this of course, requires you to also have multiple versions of the API deployed at the same time).

A common approach (and one of the most efficient) is to include the version number in the URL path.

@RestController
@RequestMapping("/api/v1")
public class UserControllerV1 {
    // Version 1 endpoints
}

@RestController
@RequestMapping("/api/v2")
public class UserControllerV2 {
    // Version 2 endpoints with updates or new features
}

Some benefits of versioning include:

  • Backward compatibility: Clients using older versions remain unaffected by changes.
  • Controlled rollouts: Gradually introduce new features and allow clients to migrate at their own pace.
  • Clear communication: Clearly indicate which version of the API is being used.

Paginate responses

When your API deals with large datasets, returning all records in a single response can lead to performance issues and increased load times. Implementing pagination helps manage data efficiently and improves the user experience.

Of course, given how Spring Data JPA is already meant for dealing with databases and APIs, it already provides built-in support for pagination through the Pageable interface.

Controller example:

import org.springframework.data.domain.Pageable; 

@GetMapping("/users")
public Page<User> getAllUsers(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "10") int size) {
    Pageable pageable = PageRequest.of(page, size);
    return userService.getAllUsers(pageable);
}

Some benefits of pagination include:

  • Performance optimization: Reduces the amount of data transferred in each request.
  • Improved user experience: Faster response times and more manageable data chunks.
  • Scalability: Handles growth in data volume without degrading performance.

Additional best practices

While the four best practices outlined above are fundamental, consider incorporating the following additional practices to further enhance your REST API.

Use consistent naming conventions

A crucial factor for API adoption is the developer experience (DX) that you provide your users. One way you can improve the DX of your API is through the use of consistent naming conventions across all your endpoints and other user-facing aspects:

  • Endpoints: Make sure your endpoints are named after your resource and use plural nouns for resource names (e.g., /users instead of /user).
  • Path variables: Clearly define and consistently use path variables (e.g., /users/{id}).

Provide comprehensive documentation

Inline with the idea of providing a good DX, make sure developers have all the tools they need to properly understand how to use your API.

Take advantage of tools like Swagger (OpenAPI) to generate interactive API documentation that they can use to get sample responses and validate their assumptions about their API.

Implement error handling

Error handling is often neglected by developers focused on the happy paths of their API endpoints. This leads to returning cryptic error messages or even providing an inconsistent format on different errors, depending on who coded it.

Consider creating a global exception handler to manage and format error responses consistently, and make sure your entire team uses it.

@ControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<?> handleResourceNotFoundException(ResourceNotFoundException ex, WebRequest request) {
        ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false));
        return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);
    }
    
    // Handle other exceptions
}

Optimize performance

If you’re starting to see longer response times on your endpoints, maybe due to high levels of traffic, or perhaps an overloaded database, consider implementing some of the following optimization techniques.

  • Caching: Implement caching strategies to reduce database load and improve response times. This can be at the webserver level, or even at the application level by caching common requests to the database. First understand where the bottlenecks are, and then make sure caching is a valid option for them.
  • Asynchronous processing: Use asynchronous methods for long-running tasks to prevent blocking requests.
  • Ensure scalability: Design your API to handle increasing loads by implementing load balancing, horizontal scaling, and efficient resource management.

Conclusion

Building a REST API with Spring Boot involves several key steps, from setting up your project and defining data models to implementing CRUD operations and securing your endpoints. By following this guide, you’ve laid the foundation for a functional and secure API that can serve as the backbone of your web or mobile applications.

As you continue developing your API, consider exploring additional Spring Boot features such as advanced security configurations, caching strategies, and asynchronous processing. Experimenting with these tools will not only deepen your understanding of Spring Boot but also enable you to build more sophisticated and efficient APIs.

Start the discussion at forum.camunda.io

Try All Features of Camunda

Related Content

See how you can solve the RPA Challenge (and much more when it comes to orchestrating your RPA bots) with Camunda.
Leverage robotic process automation (RPA) to automate tasks, enhance efficiency, and minimize human errors.
Avoid these four common pitfalls as you set up Camunda 8.