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.
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:
Requirements review
Now, suppose you are given the following database design:
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
}
GET /api/guests/{id}
{
"id": 4,
"name": "Javier",
"lastName": "Vega",
"email": "test@gmail.com"
}
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"
}
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"
}
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.
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<>();
}
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")
.
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()
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.
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.
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.
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.
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.
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.
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.
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.
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
.
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
);
}
}
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.
Thank you for reading.
Top comments (1)
Very nice and instructive ! thanks
May be out of topic, but so many classes for a "simple" CRUD, even with the help of Spring :(