Frankiie's Corner

Create a web API with Java Spring Boot

27 Feb 2023

In this tutorial I will guide you how to build a simple controller-based web API that uses Java, Spring Boot, and Spring Data JPA. If you are new to web development, specially API development, just feel relax and spend few minutes to read about what is the API, and how it works. Otherwise, let’s jump into the interesting parts.

In this tutorial, you learn how to:

  • Configure and create a API project using Java and Spring Boot
  • Add a model class for data storage using Spring Data JPA
  • Design and implement Repository and Service layers to separate persistent logic and business logic
  • Implement a controller with CRUD methods to handle HTTP request/response
  • Test the API with Curl

At the end, you have a web API that can manage note items stored in a database.

API Description Request body Response body
GET /api/notes get all notes none array of notes
GET /api/notes/{id} get a note by id none a note
POST /api/notes add a new note note item a note
PUT /api/notes/{id} update a note by id note item a note
DELETE /api/notes/{id} delete a note by id none none

The following diagram shows the design of the application.

api architecture

Prerequisites

  • Spring Boot v2.0+
  • JDK 11 or later
  • Maven 3+
  • Podman or curl (for testing api endpoints)
  • Java IDE (intelliJ, Eclipse, VSCode)

Add dependencies

From the root project, let’s open the pom.xml file and add the following dependencies into the <dependencies></dependencies> section.

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter</artifactId>
</dependency>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-devtools</artifactId>
</dependency>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
  <version>1.18.22</version>
  <scope>provided</scope>
</dependency>

Add a model class

OK, let’s get started by adding the model class, from the project you let’s create a model folder, and inside it, create a Java pojo class Note.java.

Model theoretically is a set of classes that represent the data that the application manages for persistent logic and represent for the view (UI).

The model for our application is just a single Note class.

src/main/java/com/harrykien/notes/model/Note.java

import java.util.Date;

import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

import jakarta.persistence.Basic;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.PreUpdate;
import jakarta.persistence.Table;
import jakarta.persistence.Temporal;
import jakarta.persistence.TemporalType;
import jakarta.persistence.UniqueConstraint;
import lombok.Data;

@Entity
public class Note {
    @Id
    @GeneratedValue
    private Long id;

    @Column(name = "title")
    private String title;

    @Column(name = "body")
    private String body;

    @CreationTimestamp
    @Column(name = "created_at")
    @Temporal(TemporalType.TIMESTAMP)
    private Date createdAt = new Date(); // initialize created date

    @UpdateTimestamp
    @Column(name = "updated_at")
    @Temporal(TemporalType.TIMESTAMP)
    private Date updatedAt = new Date(); // initialize updated date

    public Note() {
    }

    public Note(String title, String body) {
        this.title = title;
        this.body = body;
    }

   @PreUpdate
   public void setUpdatedAt() {   // for automatic update timestamp
       this.updatedAt= new Date();
   }
}

Here we have some JPA annotations explanation:

  • @Entity - to make this object to be persisted and managed storage in a JPA-based data storage.
  • @Id - to make this field to be a unique identifier of the entity.
  • @GeneratedValue - to provides for the specification of generation strategies for the values of primary keys.
  • @Column - emphasize this field is the column in the database.
  • @CreationTimetamp and @UpdateTimestamp - marks a property as the creation/update timestamp of the containing entity. The property value will be set to the current VM date exactly once when saving the owning entity for the first time.
  • @Temporal - specify for persistent fields or properties of type java.util.Date

Add a repository

After a model is created, we need to add a repository respectively to handle the works like data accessing, querying with that model.

A repository theoretically is an abstract layer that provides access to data sources like networks or local databases processing. It can help to separate the data access logic from the business logic of an application. Spring Data JPA repositories are interfaces with methods supporting creating, reading, updating, and deleting records against a back end data store

Ok, short explanation is enough, let’s create a repository folder, and inside it create an interface NoteRepository.java.

/src/main/java/com/harrykien/notes/repository/NoteRepository.java

import com.harrykien.notes.model.Note;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface NoteRepository extends JpaRepository<Note, Long> {
}

By extending the JpaRepository interface which has two types, one is the domain type Note and the other one is id type Long provided by Spring Data JPA, we now automatically will be able to:

  • Create new notes
  • Update existing notes
  • Delete notes
  • find notes (one or many)
  • and many more functionalities…

Add payloads

Have you ever noticed that, when we perform HTTP GET request properly, some data will be returned? And for some requests such HTTP POST or UPDATE, we mostly need to provide a request body with some data to be able to send the request huh? Yes it’s the payload.

We need a payload for request and response because it is the essential message or information required by the server to generate a response or the user to make a decision.

The payload of an API is the data provided for transporting to the server when you make an API request. It is the body of your HTTP request and response message. It may be a JSON, XML, HTML, TEXT, etc

Oki, let’s create a folder payload in the same location as model and service folders, and continually create two files NoteRequest.java for the requesting payload and NoteResponse.java for the responding payload.

/src/main/java/com/harrykien/notes/payload/NoteRequest.java

import lombok.Data;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

@Data
public class NoteRequest {
      @NotBlank
      @NotNull
      @Size(min = 5, max = 200)
      private String title;

      @NotBlank
      @Size(min = 5, max = 500)
      private String body;
}

/src/main/java/com/harrykien/notes/payload/NoteResponse.java

@Data
public class NoteResponse {
      private String title;
      private String body;
}

Add a service

So far, we have added a model for handling data presentation, a repository for handling the persistence management, and now we need a service for handling application’s business logic. Ok, let’s create a folder service and inside it create a interface NoteService.java.

/src/main/java/com/harrykien/notes/service/NoteService.java

import com.harrykien.notes.model.Note;
import com.harrykien.notes.payload.ApiResponse;
import com.harrykien.notes.payload.NoteRequest;
import com.harrykien.notes.payload.NoteResponse;

public interface NoteService {
     List<Note> getNotes();

     Note updateNote(Long id, NoteRequest request);

     String deleteNote(Long id);

     NoteResponse addNote(NoteRequest request);

     Note getNote(Long id);
}

Not over yet, we need a concrete class which will carry out the implementation of this NoteService interface, inside the service folder, create a nested folder impl, and inside impl folder, create a java class NoteServiceImpl.java.

/src/main/java/com/harrykien/notes/service/impl/NoteServiceImpl.java


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.harrykien.notes.exception.ResourceNotFoundException;
import com.harrykien.notes.model.Note;
import com.harrykien.notes.payload.ApiResponse;
import com.harrykien.notes.payload.NoteRequest;
import com.harrykien.notes.payload.NoteResponse;
import com.harrykien.notes.repository.NoteRepository;
import com.harrykien.notes.service.NoteService;

@Service
public class NoteServiceImpl implements NoteService {

	@Autowired
	private NoteRepository noteRepository;
}
  • @Service (from Spring docs)

    Indicates that an annotated class is a “Service” as “an operation offered as an interface that stands alone in the model, with no encapsulated state.” This annotation serves as a specialization of @Component, allowing for implementation classes to be auto detected through class path scanning.

Ok, now we need to provide the implementation for the not implemented service methods above.

Add a getNotes implementation

@Override
public List<Note> getNotes() {
    List<Note> response = noteRepository.findAll();
    return response;
}

Add a updateNote implementation

@Override
public NoteResponse addNote(NoteRequest request) {
    Note note = new Note();
    note.setBody(request.getBody());
    note.setTitle(request.getTitle());

    Note newPost = noteRepository.save(note);

    NoteResponse response = new NoteResponse();
    response.setTitle(newPost.getTitle());
    response.setBody(newPost.getBody());
    return response;
}

Add a deleteNote implementation

@Override
public String deleteNote(Long id) {
    Note response = noteRepository.findById(id).orElse(null);
    if (response == null) {
      return null;
    }

    noteRepository.delete(response);
    return "note has been successfully deleted";
}

Add a addNote implementation

@Override
public Note updateNote(Long id, NoteRequest request) {
     Note response = noteRepository.findById(id).orElse(null);
     response.setTitle(request.getTitle());
     response.setBody(request.getBody());

     noteRepository.save(response);
     return response;
}

Add a getNote implementation

@Override
public Note getNote(Long id) {
     return noteRepository.findById(id).orElse(null);
}

OK, done!…. wait, wait, wait, are we completely done? Still remember the design of the app above? What else are we still missing? isn’t a controller? Yes it is 😊

Add a controller

In the same location of service and repository folders, let’s create a controller folder, and inside it, continually create a file NoteController.java.

A controller is a class that intercepts incoming HTTP requests like POST, GET, UPDATE, DELETE,.., it converts the payload to internal data structure, prepares the model, and passes it to the client (UI, view) as the respective response.

/src/main/java/com/harrykien/notes/controller/NoteController.java

import com.harrykien.notes.model.Note;
import com.harrykien.notes.payload.ApiResponse;
import com.harrykien.notes.payload.NoteRequest;
import com.harrykien.notes.payload.NoteResponse;
import com.harrykien.notes.service.NoteService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import jakarta.validation.Valid;

@RestController
@RequestMapping("/api/notes")
public class NoteController {

      @Autowired
      private NoteService noteService;
}

As you noticed that, in the NoteService interface and NoteServiceImpl class, we have total 5 methods and its implementation, that means in the controller, we also need to create 5 HTTP handlers for those methods respectively.

Add a getNotes HTTP handler

@GetMapping
public ResponseEntity<List<Note>> getNotes() {
     List<Note> response = noteService.getNotes();
     return new ResponseEntity< >(response, HttpStatus.OK);
}

Add getNote HTTP handler

@GetMapping("/{id}")
public ResponseEntity<Note> getNote(@PathVariable(name = "id") Long id) {
     Note response = noteService.getNote(id);
     return new ResponseEntity<>(response, HttpStatus.OK);
}

Add a updateNote HTTP handler

@PutMapping("/{id}")
public ResponseEntity<Note> updateNote(@PathVariable(name = "id") Long id, @Valid @RequestBody NoteRequest request) {
     Note response = noteService.updateNote(id, request);
     return new ResponseEntity<>(response, HttpStatus.OK);
}

Add a deleteNote HTTP handler

@DeleteMapping("/{id}")
public ResponseEntity<String> deleteNote(@PathVariable(name = "id") Long id) {
    String response = noteService.deleteNote(id);
    return new ResponseEntity<>(response, HttpStatus.OK);
}

Add a addNote HTTP handler

@PostMapping
public ResponseEntity<NoteResponse> addNote(@Valid @RequestBody NoteRequest request) {
    NoteResponse response = noteService.addNote(request);
    return new ResponseEntity<>(response, HttpStatus.CREATED);
}

Here is some common controller annotations explanation:

  • @RestController indicates that the data processed by each method will be returned directly as the response body instead of passing to the view like @Controller does.

  • @RequestMapping(“/api/notes”) indicates the route of this controller, so that whenever from outside we access the /api/notes all actions will be redirected to this controller to process.

  • @GetMapping indicates the route with a HTTP GET capability representing for this method, however as you noticed that there is a parameter id inside it, it means we need a id value to be able to perform this request to get a specific note item.

  • @PutMapping indicates the route with HTTP PUT capability representing for this method, it also need a parameter id to be able to get and update specific note item.

  • @DeleteMapping indicates the route with HTTP DELETE capability, it needs a parameter id to get and delete specific note item.

  • @PostMapping indicates the route with HTTP POST capability, it perform creating a new note item.

  • @PathVariable - indicates that a method parameter should be bound to a URI template variable

  • @Valid - marks a property, method parameter or method return type for validation cascading

  • @RequestBody - indicates a method parameter should be bound to the body of the web request.

  • @ResponseBody - signals that this advice is rendered straight into the response body.

Add a fake database

Ok, let’s create some fake data, so when the application is fired up, some in-memory notes data will be created. From the same folder location (model, repository, service, controller,…) located, create a folder runner and inside it create a class DatabaseInitialization.java.

/src/main/java/com/harrykien/notes/runner/DatabaseInitialization.java

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DatabaseInitialization  {
    private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseInitialization.class);

  @Bean
    CommandLineRunner initFakeData(NoteRepository repository) {
        return args -> {
          LOGGER.info("Preloading " + repository.save(new Note("new note 1", "new note 1 description")));
          LOGGER.info("Preloading " + repository.save(new Note("new note 2", "new note 2 description")));
    };
  }
}

What happens when it gets loaded?

  • Spring Boot will run all CommandLineRunner beans once the application context is loaded
  • This runner will request a copy of the EmployeeRepository you just created
  • Using it, it will create two entities and store them

A fragment of console output showing preloading of data

2023-02-22T04:50:28.618+09:00  INFO 36276 --- [  restartedMain] com.harrykien.notes.NotesApplication     : Started NotesApplication in 3.067 seconds (process running for 3.372)
2023-02-22T04:50:28.670+09:00  INFO 36276 --- [  restartedMain] c.h.notes.runner.DatabaseInitialization  : Preloading Note(id=1, title=new note 1, body=new note 1 description, createdAt=Tue Feb 21 19:50:28 UTC 2023, updatedAt=Tue Feb 21 19:50:28 UTC 2023)
2023-02-22T04:50:28.672+09:00  INFO 36276 --- [  restartedMain] c.h.notes.runner.DatabaseInitialization  : Preloading Note(id=2, title=new note 2, body=new note 2 description, createdAt=Tue Feb 21 19:50:28 UTC 2023, updatedAt=Tue Feb 21 19:50:28 UTC 2023)

OK, now we completely done with the coding part. If you’ve followed along with me, you might have a project structure folder same as mine 😊.

src
├───main
│   ├───java
│   │   └───com
│   │       └───harrykien
│   │           └───notes
│   │               ├───controller
│   │               ├───model
│   │               ├───payload
│   │               ├───repository
│   │               ├───service
│   │               |   └───impl
│   └───resources
└───test
    └───java
        └───com
            └───harrykien
                └───notes

Note that project structure isn’t important much for now, so you can customize it as you wished. However having a good project structure helps you get a clear view about the project and can categorize files into the respective folder.

Oki, let’s execute the following command from the root of the project (where the pom.xml file located) to run the application.

./mvnw clean spring-boot:run

If the application get executed successfully, most likely your screen result will be the same as mine.

running application

Despite the successful execution, to make sure our app functioning properly, let’s test it.

Test the API

For testing the API (controller endpoints), you normally have few options, for those of you who prefer working with UI can download and use the tool such Postman, otherwise, just following along with me using Curl to test the API inside the command line terminal. if you don’t have Curl installed yet, you can get it here

First up, it’s considered as the best idea to set curl in verbose mode -v, as the commands provide helpful information such as the resolved IP address, the port we’re trying to connect to, and the headers. E.g. curl -v http://host:port/path

Test addNote request

curl -v -X POST -H "Content-Type: application/json" -d '{"title": "new note", "body": "new note body"}' http://localhost:8090/api/notes

In this command, we use Curl to perform HTTP POST request

  • -v as described above is the verbose mode
  • -X POST indicate the command of the HTTP request we want Curl to perform, which is POST
  • -d here indicate HTTP POST data which is our request body '{"title":"new note","body":"new note body"}'
  • -H is indicate the content type of the request which is application/json

Output:

image

Test getNotes request

curl -v http://localhost:8090/api/notes

This command perform get all available notes item from database, it’s simple right?

Output:

image

Test a updateNote by id request

curl -v -X PUT http://localhost:8090/api/notes/5 -d '{"title": "update note", "body": "update body"}' -H 'Content-Type: application/json'

This command perform HTTP PUT request, the structure is almost the same as the POST request.

Output:

image

Test getNote by id request

curl -v http://localhost:8090/api/notes/5

This command perform HTTP GET by id equal to 5

Output:

image

Test deleteNote by id request

curl -v -X DELETE http://localhost:8090/api/notes/5

This command perform HTTP DELETE request by id equal to 5

Output

image

Conclusion

In this tutorial, you learned how to build a web API service using Java and Spring Boot. Make multiple application layers such Model for data management, Repository for persistent logic, Service for business logic, and Controller for handling the HTTP request/response, finally we tested it via Curl. It was a long article but we finished it. I hope you enjoy this and found something helpful 😊.