Creating a Movie API with Spring Boot and PostgreSQL
Written on
Chapter 1: Introduction
In this tutorial, we will develop a Spring Boot application named Movie API. By utilizing Spring Data JPA, this API will interact with a PostgreSQL database. The Movie API will provide several endpoints:
- GET /api/movies
- GET /api/movies/{imdbId}
- POST /api/movies {"imdbId": "...", "title": "...", "year": ..., "actors": "..."}
- PATCH /api/movies/{imdbId} {"title": "...", "year": ..., "actors": "..."}
- DELETE /api/movies/{imdbId}
This API will serve as a foundation for future articles, where we will explore unit and integration testing, caching mechanisms, and more.
Without further delay, let’s dive in!
Section 1.1: Prerequisites
To follow along, ensure that you have Java 17 or later and Docker installed on your machine.
Subsection 1.1.1: Setting Up the Movie API Spring Boot Application
We will initiate our Spring Boot application using Spring Initializr. The project will be named movie-api, and the required dependencies include Spring Web, Spring Data JPA, PostgreSQL Driver, Lombok, and Validation. We will utilize Spring Boot version 3.1.4 along with Java 17.
Please follow this link for the setup instructions. After clicking the GENERATE button, download the zip file, extract it to your preferred directory, and open the movie-api project in your IDE.
Section 1.2: Structuring the Project
To keep our code well-organized, we will create the following packages within the com.example.movieapi root package: controller, exception, mapper, model, repository, and service.
Chapter 2: Creating the Movie Model
In the model package, create the Movie entity class with the following content:
package com.example.movieapi.model;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Data
@Entity
@Table(name = "movies")
public class Movie {
@Id
private String imdbId;
private String title;
private Integer year;
private String actors;
}
The Movie class represents the movies table in the PostgreSQL database.
Section 2.1: Building the Repository
Next, in the repository package, create the MovieRepository interface:
package com.example.movieapi.repository;
import com.example.movieapi.model.Movie;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MovieRepository extends JpaRepository<Movie, String> {
}
This interface extends JpaRepository for managing Movie entities, providing common database operations like saving, updating, deleting, and querying.
Section 2.2: Exception Handling
In the exception package, create the MovieNotFoundException class:
package com.example.movieapi.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.NOT_FOUND)
public class MovieNotFoundException extends RuntimeException {
public MovieNotFoundException(String message) {
super(message);}
}
This exception is thrown when a request is made for a movie that doesn't exist.
Chapter 3: Service Layer Implementation
Section 3.1: Service Interface
In the service package, define the MovieService interface:
package com.example.movieapi.service;
import com.example.movieapi.model.Movie;
import java.util.List;
public interface MovieService {
List<Movie> getMovies();
Movie validateAndGetMovieById(String imdbId);
Movie saveMovie(Movie movie);
void deleteMovie(Movie movie);
}
Section 3.2: Service Implementation
Also in the service package, implement the MovieServiceImpl class:
package com.example.movieapi.service;
import com.example.movieapi.exception.MovieNotFoundException;
import com.example.movieapi.model.Movie;
import com.example.movieapi.repository.MovieRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@RequiredArgsConstructor
@Service
public class MovieServiceImpl implements MovieService {
private final MovieRepository movieRepository;
@Override
public List<Movie> getMovies() {
return movieRepository.findAll();}
@Override
public Movie validateAndGetMovieById(String imdbId) {
return movieRepository.findById(imdbId)
.orElseThrow(() -> new MovieNotFoundException("Movie with id '%s' not found".formatted(imdbId)));}
@Override
public Movie saveMovie(Movie movie) {
return movieRepository.save(movie);}
@Override
public void deleteMovie(Movie movie) {
movieRepository.delete(movie);}
}
The MovieService interface and its implementation manage movie operations like retrieving, validating, saving, and deleting movies.
Section 3.3: Data Transfer Objects (DTO)
In the controller package, create a new package named dto. Inside, we will define three DTO records: CreateMovieRequest, UpdateMovieRequest, and MovieResponse.
Starting with the CreateMovieRequest record:
package com.example.movieapi.controller.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
public record CreateMovieRequest(
@NotBlank String imdbId,
@NotBlank String title,
@NotNull Integer year,
@NotBlank String actors) {
}
Next, create the UpdateMovieRequest record:
package com.example.movieapi.controller.dto;
public record UpdateMovieRequest(String title, Integer year, String actors) {
}
Finally, define the MovieResponse record:
package com.example.movieapi.controller.dto;
import java.io.Serializable;
public record MovieResponse(String imdbId, String title, Integer year, String actors) implements Serializable {
}
These records will be used in the controller to structure the request and response data.
Chapter 4: Mapper Implementation
Section 4.1: Creating the Mapper
In the mapper package, define the MovieMapper interface:
package com.example.movieapi.mapper;
import com.example.movieapi.controller.dto.CreateMovieRequest;
import com.example.movieapi.controller.dto.MovieResponse;
import com.example.movieapi.controller.dto.UpdateMovieRequest;
import com.example.movieapi.model.Movie;
public interface MovieMapper {
Movie toMovie(CreateMovieRequest createMovieRequest);
void updateMovieFromUpdateMovieRequest(Movie movie, UpdateMovieRequest updateMovieRequest);
MovieResponse toMovieResponse(Movie movie);
}
Section 4.2: Mapper Implementation
Next, implement the MovieMapperImpl class:
package com.example.movieapi.mapper;
import com.example.movieapi.controller.dto.CreateMovieRequest;
import com.example.movieapi.controller.dto.MovieResponse;
import com.example.movieapi.controller.dto.UpdateMovieRequest;
import com.example.movieapi.model.Movie;
import org.springframework.stereotype.Service;
@Service
public class MovieMapperImpl implements MovieMapper {
@Override
public Movie toMovie(CreateMovieRequest createMovieRequest) {
if (createMovieRequest == null) {
return null;}
Movie movie = new Movie();
movie.setImdbId(createMovieRequest.imdbId());
movie.setTitle(createMovieRequest.title());
movie.setYear(createMovieRequest.year());
movie.setActors(createMovieRequest.actors());
return movie;
}
@Override
public void updateMovieFromUpdateMovieRequest(Movie movie, UpdateMovieRequest updateMovieRequest) {
if (updateMovieRequest == null) {
return;}
if (updateMovieRequest.title() != null) {
movie.setTitle(updateMovieRequest.title());}
if (updateMovieRequest.year() != null) {
movie.setYear(updateMovieRequest.year());}
if (updateMovieRequest.actors() != null) {
movie.setActors(updateMovieRequest.actors());}
}
@Override
public MovieResponse toMovieResponse(Movie movie) {
if (movie == null) {
return null;}
return new MovieResponse(movie.getImdbId(), movie.getTitle(), movie.getYear(), movie.getActors());
}
}
The MovieMapper interface and its implementation handle the mapping between the DTO classes and the Movie entity.
Chapter 5: Controller Implementation
In the controller package, create the MovieController class:
package com.example.movieapi.controller;
import com.example.movieapi.controller.dto.CreateMovieRequest;
import com.example.movieapi.controller.dto.MovieResponse;
import com.example.movieapi.controller.dto.UpdateMovieRequest;
import com.example.movieapi.mapper.MovieMapper;
import com.example.movieapi.model.Movie;
import com.example.movieapi.service.MovieService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RequiredArgsConstructor
@RestController
@RequestMapping("/api/movies")
public class MovieController {
private final MovieService movieService;
private final MovieMapper movieMapper;
@GetMapping
public List<MovieResponse> getMovies() {
return movieService.getMovies()
.stream()
.map(movieMapper::toMovieResponse)
.toList();
}
@GetMapping("/{imdbId}")
public MovieResponse getMovie(@PathVariable String imdbId) {
Movie movie = movieService.validateAndGetMovieById(imdbId);
return movieMapper.toMovieResponse(movie);
}
@ResponseStatus(HttpStatus.CREATED)
@PostMapping
public MovieResponse createMovie(@Valid @RequestBody CreateMovieRequest createMovieRequest) {
Movie movie = movieService.saveMovie(movieMapper.toMovie(createMovieRequest));
return movieMapper.toMovieResponse(movie);
}
@PatchMapping("/{imdbId}")
public MovieResponse updateMovie(@PathVariable String imdbId, @RequestBody UpdateMovieRequest updateMovieRequest) {
Movie movie = movieService.validateAndGetMovieById(imdbId);
movieMapper.updateMovieFromUpdateMovieRequest(movie, updateMovieRequest);
movie = movieService.saveMovie(movie);
return movieMapper.toMovieResponse(movie);
}
@DeleteMapping("/{imdbId}")
public void deleteMovie(@PathVariable String imdbId) {
Movie movie = movieService.validateAndGetMovieById(imdbId);
movieService.deleteMovie(movie);
}
}
The MovieController manages HTTP requests for movie records, enabling users to create, update, delete, and retrieve movie data.
Section 5.1: Updating Application Properties
Update the application.properties file with the following configuration:
spring.application.name=movie-api
spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=jdbc:postgresql://${POSTGRES_HOST:localhost}:${POSTGRES_PORT:5432}/moviesdb
spring.datasource.username=postgres
spring.datasource.password=postgres
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.orm.jdbc.bind=TRACE
This section details the configuration properties for the application, including database connection and logging levels.
Section 5.2: Starting the Environment
To run PostgreSQL, execute the following command in your terminal:
docker run -d --name postgres
-p 5432:5432
-e POSTGRES_USER=postgres
-e POSTGRES_PASSWORD=postgres
-e POSTGRES_DB=moviesdb
postgres:15.4
Next, navigate to the root directory of the movie-api project and start the application with:
./mvnw clean spring-boot:run
Section 5.3: Testing the Endpoints
To interact with the Movie API endpoints, use the following cURL commands.
Retrieve all movies:
curl -i localhost:8080/api/movies
The expected response (with no movies registered) should be:
HTTP/1.1 200
...
[]
To create a movie:
curl -i -X POST localhost:8080/api/movies
-H 'Content-Type: application/json'
-d '{"imdbId": "tt9783600", "title": "Spiderhead", "year": 2022, "actors": "Chris Hemsworth, Miles Teller, Jurnee Smollett"}'
The expected response should be:
HTTP/1.1 201
...
{"imdbId":"tt9783600","title":"Spiderhead","year":2022,"actors":"Chris Hemsworth, Miles Teller, Jurnee Smollett"}
To update the movie's year:
curl -i -X PATCH localhost:8080/api/movies/tt9783600
-H 'Content-Type: application/json'
-d '{"year": 2022}'
You should receive:
HTTP/1.1 200
...
{"imdbId":"tt9783600","title":"Spiderhead","year":2022,"actors":"Chris Hemsworth, Miles Teller, Jurnee Smollett"}
To retrieve the movie:
curl -i localhost:8080/api/movies/tt9783600
The expected response is:
HTTP/1.1 200
...
{"imdbId":"tt9783600","title":"Spiderhead","year":2022,"actors":"Chris Hemsworth, Miles Teller, Jurnee Smollett"}
Finally, to delete the movie:
curl -i -X DELETE localhost:8080/api/movies/tt9783600
The response should confirm deletion with:
HTTP/1.1 200
...
If you attempt to retrieve a non-existent movie:
curl -i localhost:8080/api/movies/tt9783600
You should receive:
HTTP/1.1 404
...
{"timestamp":"2023-09-04T08:19:56.883+00:00","status":404,"error":"Not Found","path":"/api/movies/tt9783600"}
Section 5.4: Shutting Down
To stop the Movie API application, press Ctrl+C in the terminal where it is running. To stop the PostgreSQL Docker container, run:
docker rm -fv postgres
Chapter 6: Conclusion
In this guide, we've successfully built a Spring Boot application known as Movie API. The application stores movie data in PostgreSQL and employs Spring Data JPA for database interactions. We also tested the application's endpoints to ensure they function correctly.
Chapter 7: Additional Resources
- Implementing Unit Tests for a Spring Boot API using Spring Data JPA and PostgreSQL
A step-by-step guide on creating unit tests for the Movie API with the Spring Testing Library.
- Implementing Integration Tests for a Spring Boot API using Spring Data JPA and PostgreSQL
A guide on crafting integration tests for the Movie API using Testcontainers.
- Implementing Caching with Redis in a Spring Boot API using Spring Data JPA and PostgreSQL
Learn how to add caching to the Movie API using Redis.
- Implementing Caching with Caffeine in a Spring Boot API using Spring Data JPA and PostgreSQL
A guide on utilizing Caffeine for caching in the Movie API.
- Configuring OpenAPI in a Spring Boot API using Spring Data JPA and PostgreSQL
Steps for setting up OpenAPI in the Movie API.
- Exposing Metrics in a Spring Boot API using Spring Data JPA and PostgreSQL
A guide to configuring Actuator and Prometheus metrics for the Movie API.
- Running Prometheus and Grafana to Monitor a Spring Boot API Application
Instructions to run Prometheus and Grafana locally using Docker Compose for monitoring Movie API metrics.
- Running Movie API in Minikube (Kubernetes) using Spring Data JPA and PostgreSQL
Learn how to deploy the Movie API in a Minikube environment.
Support and Engagement
If you found this article helpful, please consider showing your support by engaging with it. Feel free to clap, highlight, and ask questions. Share this article on social media, follow me on Medium, LinkedIn, and Twitter, and subscribe to my newsletter to stay updated on future posts.