Basic REST API with Spring Boot 2023
Tags: springboot, java, beginners, api
Published: 2023-09-07 10:18:48

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](https://start.spring.io/).

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/733hxbivhx3c7gd9gbx8.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ss29zkpk04w156460xsa.png)

## Requirements review

Now, suppose you are given the following database design:
![Database diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gdx9s8hidgp8ow6pu7pw.png)

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`

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

GET `/api/guests/{id}`

```json
{
  "id": 4,
  "name": "Javier",
  "lastName": "Vega",
  "email": "test@gmail.com"
}
```

POST `/api/guests`

```json
// Body
{
    "name": "Javier",
    "lastName": "Vega",
    "email": "test@gmail.com",
    "password": "1234"
}
// Response
{
    "id": 5,
    "name": "Javier",
    "lastName": "Vega",
    "email": "test@gmail.com"
}
```

PUT `/api/guests/{id}`

```json
// Body
{
    "name": "Javier A.",
    "lastName": "Vega",
    "email": "test@gmail.com",
    "password": "1234"
}
// Response
{
    "id": 5,
    "name": "Javier A.",
    "lastName": "Vega",
    "email": "test@gmail.com"
}
```

DELETE `/api/guests/{id}`

```
Guest deleted
```

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
```

### 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.

```java
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<>();
}

```

```java
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;
}
```

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")`.

```java
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;
}
```

### 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()

```java
package com.hotel.api.repository;

public interface GuestRepository extends JpaRepository<Guest, Integer>
{
}
```

### Service Interfaces

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

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

### 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.

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

### 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.

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

### 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.

```java
package com.hotel.api.dto;

@Data
public class GuestDto {
    private int id;
    private String name;
    private String lastName;
    private String email;
}
```

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

```java
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;
}
```

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

```java
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;
}
```

### 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.

```java
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;
    }
}
```

### 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.

```java
package com.hotel.api.exception;

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

    public GuestNotFoundException(String message){
        super(message);
    }
}
```

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`.

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

```java
package com.hotel.api.exception;

@Data
public class ErrorObject {
    private Integer statusCode;
    private String message;
    private Date timestamp;
}
```

## 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](https://github.com/JavierVMC29/Spring-Boot-Hotel-REST-API).

Thank you for reading.