DEV Community

Javier Vega
Javier Vega

Posted on

Basic REST API with Spring Boot 2023

In this tutorial I will show you how to develop a very basic REST API using Spring Boot with Java and Maven.

Setup project

First, you need to create a Spring Boot project. To do it you can use Spring Initializr.

Add the following dependencies:

  • Lombok
  • Spring Web
  • Spring Data JPA
  • PostgreSQL Driver

Then, click generate to download a zip with the project.

Spring Initializr set up

Unzip the project and open it with an IDE of your preference, I am going to use IntelliJ IDEA Community Edition 2022.1.2.

Run the ApiApplication.java file:

IDE after run ApiApplication.java

Requirements review

Now, suppose you are given the following database design:
Database diagram

And you are required to create a REST API with the following endpoints for each entity:

Guest:

Method Endpoint
GET /api/guests
GET /api/guests/{id}
POST /api/guests
PUT /api/guests/{id}
DELETE /api/guests/{id}

Room:

Method Endpoint
GET /api/rooms
GET /api/rooms/{id}
POST /api/rooms
PUT /api/rooms/{id}
DELETE /api/rooms/{id}

Reservation:

Method Endpoint
GET /api/reservations
GET /api/reservations/{id}
POST /api/reservations
PUT /api/reservations/{id}
DELETE /api/reservations/{id}

Example of responses:
GET /api/guests

{
  "content": [
    {
      "id": 4,
      "name": "Javier",
      "lastName": "Vega",
      "email": "test@gmail.com"
    }
  ],
  "pageNo": 0,
  "pageSize": 10,
  "totalElements": 1,
  "totalPages": 1,
  "last": true
}
Enter fullscreen mode Exit fullscreen mode

GET /api/guests/{id}

{
  "id": 4,
  "name": "Javier",
  "lastName": "Vega",
  "email": "test@gmail.com"
}
Enter fullscreen mode Exit fullscreen mode

POST /api/guests

// Body
{
    "name": "Javier",
    "lastName": "Vega",
    "email": "test@gmail.com",
    "password": "1234"
}
// Response
{
    "id": 5,
    "name": "Javier",
    "lastName": "Vega",
    "email": "test@gmail.com"
}
Enter fullscreen mode Exit fullscreen mode

PUT /api/guests/{id}

// Body
{
    "name": "Javier A.",
    "lastName": "Vega",
    "email": "test@gmail.com",
    "password": "1234"
}
// Response
{
    "id": 5,
    "name": "Javier A.",
    "lastName": "Vega",
    "email": "test@gmail.com"
}
Enter fullscreen mode Exit fullscreen mode

DELETE /api/guests/{id}

Guest deleted
Enter fullscreen mode Exit fullscreen mode

The database must be implemented using PostgreSQL.

Start coding

Project directory structure

Create the following packages inside com.hotel.api:

  • controller
  • dto
  • exception
  • mapper
  • model
  • repository
  • service

Project configuration

We need to use a PostgreSQL database so I have created one in my local machine. To connect our Spring Boot app to the database we need to add the following properties to the application.properties file:

spring.datasource.url=jdbc:postgresql://localhost:5432/hotel
spring.datasource.username=postgres
spring.datasource.password=1234
spring.datasource.driver-class-name=org.postgresql.Driver

# Testing
# This will drop any table in the database
# and create new ones base on the models
spring.jpa.hibernate.ddl-auto=create-drop

# Development
# This will update table schemas base on the models,
# but not will not remove columns that no longer exist
# in the models, it will just add new columns if needed.
#spring.jpa.hibernate.ddl-auto=update

# Production
#spring.jpa.hibernate.ddl-auto=none

# Show generated queries in logs - Spring Boot uses logback
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
Enter fullscreen mode Exit fullscreen mode

Models

To create a model representing a table in our database we need to use the annotation @Entity. By default, the name of the table will be the class name in lowercase. If you want a custom name for your table use @Table(name = "your_table_name")

Each attribute of the class will correspond to a column in the table. By default, the column names will be the same as the attributes. If you want the column to have a different name from the attribute name use @Column(name = "your_column_name").

To make an attribute correspond to the PK of the table we need to use @Id. If you want the value to be auto-generated use @GeneratedValue(strategy = GenerationType.IDENTITY).

Since a guest can have many reservations, we need to use the annotation @OneToMany(mappedBy = "guest", cascade = CascadeType.ALL, orphanRemoval = true). This will make it possible to fetch the guest along with all their reservations in the future.

package com.hotel.api.model;

@Data // Add getters and setters for all attributes
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class Guest {
    @Id // Make the attribute the PK of the table.
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;
    private String name;
    @Column(name = "last_name")
    private String lastName;
    private String email;
    private String password;

    @OneToMany(mappedBy = "guest",
               cascade = CascadeType.ALL,
               orphanRemoval = true)
    private List<Reservation> reservations = new ArrayList<>();
}

Enter fullscreen mode Exit fullscreen mode
package com.hotel.api.model;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class Room {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;
    @Column(unique = true)
    private String number;
    private String type;
}
Enter fullscreen mode Exit fullscreen mode

The reservation entity is associated with a room and a guest. If you want to query the data of these entities along with the Reservation data, you need to use @ManyToOne(fetch = FetchType.LAZY) and @JoinColumn(name = "FK_column_name").

package com.hotel.api.model;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class Reservation {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;
    private String title;
    @Column(name = "date_start")
    private LocalDate dateStart;
    @Column(name = "date_end")
    private LocalDate dateEnd;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "room_id")
    private Room room;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "guest_id")
    private Guest guest;
}
Enter fullscreen mode Exit fullscreen mode

Repositories

To create a Repository we only need to create an interface that extends JpaRepository<Entity, IdDataType>. This interface will provide us with some methods for querying the database like:

  • findAll()
  • findById()
  • save()
  • delete()
package com.hotel.api.repository;

public interface GuestRepository extends JpaRepository<Guest, Integer>
{
}
Enter fullscreen mode Exit fullscreen mode

Service Interfaces

Now, we need to create an interface with the definition of all the methods that we wish to implement in our service.

package com.hotel.api.service;

public interface GuestService {
    GuestDto createGuest(Guest guest);
    GuestsResponse getAllGuests(int pageNo, int pageSize);
    GuestDto getGuestById(int id);
    GuestDto updateGuest(GuestDto guestDto, int id);
    void deleteGuestById(int id);

    GuestReservationsResponse getGuestReservations(int id);
}
Enter fullscreen mode Exit fullscreen mode

Services Implementation

Services are classes with the implementation to query the data. To make a class a service you need to use the annotation @Service. Then, implements the interface of the service and overrides all its methods.

package com.hotel.api.service.impl;

@Service
public class GuestServiceImpl implements GuestService {

    private final GuestRepository guestRepository;

    @Autowired
    public GuestServiceImpl(GuestRepository guestRepository) {
        this.guestRepository = guestRepository;
    }

    @Override
    public GuestDto createGuest(Guest guest) {
        Guest newGuest = guestRepository.save(guest);
        return GuestMapper.mapToDto(newGuest);
    }

    @Override
    public GuestsResponse getAllGuests(int pageNo, int pageSize) {
        Pageable pageable = PageRequest.of(pageNo, pageSize);
        Page<Guest> guests = guestRepository.findAll(pageable);
        List<Guest> listOfGuests = guests.getContent();
        List<GuestDto> content = listOfGuests.stream()
        .map(GuestMapper::mapToDto)
        .collect(Collectors.toList());

        GuestsResponse guestsResponse = new GuestsResponse();
        guestsResponse.setContent(content);
        guestsResponse.setPageNo(guests.getNumber());
        guestsResponse.setPageSize(guests.getSize());
        guestsResponse.setTotalElements(guests.getTotalElements());
        guestsResponse.setTotalPages(guests.getTotalPages());
        guestsResponse.setLast(guests.isLast());

        return guestsResponse;
    }

    @Override
    public GuestDto getGuestById(int id) {
        Guest guest = guestRepository.findById(id)
        .orElseThrow(
            () -> new GuestNotFoundException("Guest could not be found")
        );
        return GuestMapper.mapToDto(guest);
    }

    @Override
    public GuestDto updateGuest(GuestDto guestDto, int id) {
        Guest guest = guestRepository.findById(id)
        .orElseThrow(
            () -> new GuestNotFoundException("Guest could not be found")
        );

        guest.setName(guestDto.getName());
        guest.setLastName(guestDto.getLastName());
        guest.setEmail(guestDto.getEmail());

        Guest updatedGuest = guestRepository.save(guest);

        return GuestMapper.mapToDto(updatedGuest);
    }

    @Override
    public void deleteGuestById(int id) {
        Guest guest = guestRepository.findById(id)
        .orElseThrow(
            () -> new GuestNotFoundException("Guest could not be found")
        );
        guestRepository.delete(guest);
    }

    @Override
    public GuestReservationsResponse getGuestReservations(int id) {
        Guest guest = guestRepository.findById(id)
        .orElseThrow(
            () -> new GuestNotFoundException("Guest could not be found")
        );
        return GuestMapper.mapToGuestReservationsResponse(guest);
    }
}
Enter fullscreen mode Exit fullscreen mode

Controllers

Controllers are the last layer in the Spring Boot app, here is where you define the endpoints available in your REST API. To create a controller you need a class with the annotations @RestController and @RequestMapping("/api/"). What you put inside @RequestMapping will be added to all the mappings inside your controller.

For example, I have a method getGuests with the annotation @GetMapping("guests") so the endpoint that will call this method will be /api/guests.

All the methods mapped to an endpoint should return a ResponseEntity object. It will contain our data along with other properties like the status code.

package com.hotel.api.controller;

@RestController
@RequestMapping("/api/")
public class GuestController {
    private final GuestService guestService;

    @Autowired
    public GuestController(GuestService guestService) {
        this.guestService = guestService;
    }

    @GetMapping("guests")
    public ResponseEntity<GuestsResponse> getGuests(
            @RequestParam(
                value = "pageNo",
                defaultValue = "0",
                required = false) int pageNo,
            @RequestParam(
                value = "pageSize",
                defaultValue = "10",
                required = false) int pageSize
    ){
        return new ResponseEntity<>(
            guestService.getAllGuests(pageNo, pageSize), HttpStatus.OK
        );
    }

    @GetMapping("guests/{id}")
    public ResponseEntity<GuestDto> guestDetail(
        @PathVariable int id){
        return new ResponseEntity<>(
            guestService.getGuestById(id), HttpStatus.OK
        );
    }

    @PostMapping("guests")
    @ResponseStatus(HttpStatus.CREATED)
    public ResponseEntity<GuestDto> createGuest(
        @RequestBody Guest guest){
        return new ResponseEntity<>(
            guestService.createGuest(guest), HttpStatus.CREATED);
    }

    @PutMapping("guests/{id}")
    public ResponseEntity<GuestDto> updateGuest(
        @RequestBody GuestDto guestDto,
        @PathVariable("id") int guestId
    ){
        GuestDto response = guestService.updateGuest(guestDto, guestId);
        return new ResponseEntity<>(response, HttpStatus.OK);
    }

    @DeleteMapping("guests/{id}")
    public ResponseEntity<String> deleteGuest(
        @PathVariable("id") int guestId
    ){
        guestService.deleteGuestById(guestId);
        return new ResponseEntity<>("Guest deleted", HttpStatus.OK);
    }

    @GetMapping("guests/{id}/reservations")
    public ResponseEntity<GuestReservationsResponse> guestReservations(
        @PathVariable int id
    ){
        return new ResponseEntity<>(
            guestService.getGuestReservations(id), HttpStatus.OK
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

DTOs (Data Transfer Objects)

Throughout the code, you should see that I used a class guestDto. DTO classes are intended to be an intermediary between the entities and the data that is sent to the client.

For example, the Guest model has an attribute password. When we query the data from the database it will bring all the attributes by default, but I don't want to send them all to the client. So, I need a class that contains only the necessary attributes. This is when DTO classes come into play.

package com.hotel.api.dto;

@Data
public class GuestDto {
    private int id;
    private String name;
    private String lastName;
    private String email;
}
Enter fullscreen mode Exit fullscreen mode

I also used another DTO called GuestReservationsResponse to send the guest data along with all their reservations.

package com.hotel.api.dto;

@Data
public class GuestReservationsResponse{
    private int id;
    private String name;
    private String lastName;
    private String email;
    private List<ReservationInfo> reservations;
}
Enter fullscreen mode Exit fullscreen mode

Finally, I used another DTO called GuestsResponse to send the guests using pagination.

package com.hotel.api.dto;

@Data
public class GuestsResponse {
    private List<GuestDto> content;
    private int pageNo;
    private int pageSize;
    private long totalElements;
    private int totalPages;
    private boolean last;
}
Enter fullscreen mode Exit fullscreen mode

Mappers

To make conversions between entities and DTOs I used mappers which are classes that have methods to convert entity models to DTOs and vice versa.

In this case, I created a class GuestMapper with the methods mapToDto and mapToGuestReservationsResponse since I need to do those conversions frequently.

package com.hotel.api.mapper;

@NoArgsConstructor
public class GuestMapper {
    public static GuestDto mapToDto(Guest guest){
        GuestDto guestDto = new GuestDto();
        guestDto.setId(guest.getId());
        guestDto.setName(guest.getName());
        guestDto.setLastName(guest.getLastName());
        guestDto.setEmail(guest.getEmail());
        return guestDto;
    }

    public static GuestReservationsResponse mapToGuestReservationsResponse(Guest guest){
        GuestReservationsResponse guestReservationsResponse = new GuestReservationsResponse();

        List<ReservationInfo> reservationsInfo = ReservationMapper.mapElementsToReservationInfo(
            guest.getReservations()
        );

        guestReservationsResponse.setId(guest.getId());
        guestReservationsResponse.setName(guest.getName());
        guestReservationsResponse.setLastName(
            guest.getLastName()
        );
        guestReservationsResponse.setEmail(guest.getEmail());
        guestReservationsResponse.setReservations(reservationsInfo);

        return guestReservationsResponse;
    }
}
Enter fullscreen mode Exit fullscreen mode

Error Handling

Throughout the code, you should see that I used a class GuestNotFoundException. This class will be thrown as an exception and will be handled by Spring Boot which will send an error response to the client.

package com.hotel.api.exception;

public class GuestNotFoundException extends RuntimeException{
    @Serial
    private static final long serialVersionUID = 1;

    public GuestNotFoundException(String message){
        super(message);
    }
}
Enter fullscreen mode Exit fullscreen mode

To add error handling to our Spring Boot app we need to create a class with the annotation @ControllerAdvice and inside this class, we need to create a method that will handle an exception and send an error response to the client.

The method we define needs to be annotated with @ExceptionHandler(ExceptionClass.class) the ExceptionClass should be a class that extends Exception. Also, our class needs to return a ResponseEntity<ErrorObject> where ErrorObject can be any class we want it to be.

In this specific case ExceptionClass is GuestNotFoundException and ErrorObject is ErrorObject.

package com.hotel.api.exception;

@ControllerAdvice
public class GlobalException {
    @ExceptionHandler(GuestNotFoundException.class)
    public ResponseEntity<ErrorObject> handleGuestNotFoundException(
        GuestNotFoundException ex, WebRequest request
    ){
        ErrorObject errorObject = new ErrorObject();

        errorObject.setStatusCode(HttpStatus.NOT_FOUND.value());
        errorObject.setMessage(ex.getMessage());
        errorObject.setTimestamp(new Date());

        return new ResponseEntity<ErrorObject>(
            errorObject, HttpStatus.NOT_FOUND
        );
    }
}
Enter fullscreen mode Exit fullscreen mode
package com.hotel.api.exception;

@Data
public class ErrorObject {
    private Integer statusCode;
    private String message;
    private Date timestamp;
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

That's it. Now, we have a basic REST API working in Spring Boot. I have explained the basics of building REST APIs with Spring Boot. The code for the room and reservation repositories, services, and controllers are almost the same as the guest. You can see all the source code on my GitHub.

Thank you for reading.

Top comments (1)

Collapse
 
stephgrino profile image
Stephane Pellegrino

Very nice and instructive ! thanks

May be out of topic, but so many classes for a "simple" CRUD, even with the help of Spring :(