johnburnsonline.com

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.

Share the page:

Twitter Facebook Reddit LinkIn

-----------------------

Recent Post:

Embracing Solitude: The Path to Self-Discovery and Authenticity

Explore how self-awareness and solitude can lead to a fulfilling life and deeper understanding of oneself.

The Fascinating Link Between Exercise and Orgasms Explained

Discover the unexpected relationship between exercise and orgasms, including insights from experts and ways to enhance your experience.

# Embracing Divorce: Lessons Learned from Marriage Counseling

A personal reflection on how marriage counseling led to divorce but ultimately became a path to self-discovery and empowerment.