Build a Book Catalog REST API

Build a Spring Boot REST API with JPA, Hibernate, and AOP logging.

Introduction

30 Second Summary

Every time you search for a book online, browse a library catalog, or check your reading list on an app, an API is doing the work behind the scenes. It takes your request, pulls the right data from a database, and sends back exactly what you need.

In this project, you will build a Book Catalog REST API using Spring Boot that can create, read, update, and delete books stored in a database. Along the way, you will see how Spring's core components connect to form a real working application.

What You'll Build

A layered Spring Boot application that exposes RESTful endpoints for managing a book catalog, backed by an H2 in-memory database with automatic logging powered by Spring AOP.

By the end of this project, you'll have:

  • A Spring Boot application with a layered architecture (Controller, Service, Repository) wired together using dependency injection.
  • A JPA entity mapped to a database table via Hibernate annotations, with a Spring Data JPA repository that auto-generates queries from method names.
  • A full CRUD REST API built with Spring MVC and a custom AOP logging aspect that automatically logs every method call without touching business code.
  • Secret Mission: Add a paginated search endpoint with sorting using Spring Data's Pageable interface.

Are there any prerequisites?

You should have basic Java knowledge (classes, interfaces, and annotations). Familiarity with REST APIs (GET, POST, PUT, DELETE) is helpful but not required.

Set Up Your Development Environment

Spring Boot requires three core tools to build and run Java applications: a Java Development Kit (JDK), a build tool to manage dependencies, and an IDE to write code.

In this step, you will install JDK 17, Maven, and IntelliJ IDEA Community Edition. These three tools form the foundation for every Spring Boot project you build.

In this step, get ready to:
  • Install JDK 17 and verify it with a terminal command.
  • Install Maven and verify it with a terminal command.
  • Install IntelliJ IDEA Community Edition and confirm it detects your JDK.
Install JDK 17

The JDK (Java Development Kit) is the compiler and runtime that turns your Java source code into a running application. Spring Boot requires Java SDK version 17 or higher.

  • Open a terminal. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Check if Java is already installed by running this command:
java -version

✔️ I see version 17 or higher

Your output shows something like openjdk version "17.x.x". You are all set with Java.

  • Skip ahead to the Maven section below.

ⓧ I see an older version

Your system has a Java version below 17. You need to install JDK 17 alongside it or replace it.

The Spring team recommends BellSoft Liberica JDK 17. Follow the instructions for your operating system below.

macOS

SDKMAN is a version manager for Java-based tools. It lets you install and switch between JDK versions easily.

  • Install SDKMAN by running this command:
curl -s "https://get.sdkman.io" | bash
  • Open a new terminal window (so SDKMAN loads into your session).
  • Install Liberica JDK 17 by running:
sdk install java 17.0.19-librca
  • When prompted to set this as the default, enter Y.
  • Verify the install by running:
java -version

You should see output containing openjdk version "17 followed by a version number.

Windows

  • Open the BellSoft downloads page in your browser:

https://bell-sw.com/pages/downloads/

  • Select JDK 17 LTS from the version list on the left.
  • Under Windows, click Download next to the MSI installer for your architecture (x86 64-bit for most systems).
  • Run the downloaded MSI installer and follow the prompts. Accept the defaults.
  • Close and reopen PowerShell, then verify by running:
java -version

You should see output containing openjdk version "17 followed by a version number.

ⓧ Command not found

Java is not installed on your machine. The Spring team recommends BellSoft Liberica JDK 17.

macOS

SDKMAN is a version manager for Java-based tools. It lets you install and switch between JDK versions easily.

  • Install SDKMAN by running this command:
curl -s "https://get.sdkman.io" | bash
  • Open a new terminal window (so SDKMAN loads into your session).
  • Install Liberica JDK 17 by running:
sdk install java 17.0.19-librca
  • When prompted to set this as the default, enter Y.
  • Verify the install by running:
java -version

You should see output containing openjdk version "17 followed by a version number.

Windows

  • Open the BellSoft downloads page in your browser:

https://bell-sw.com/pages/downloads/

  • Select JDK 17 LTS from the version list on the left.
  • Under Windows, click Download next to the MSI installer for your architecture (x86 64-bit for most systems).
  • Run the downloaded MSI installer and follow the prompts. Accept the defaults.
  • Close and reopen PowerShell, then verify by running:
java -version

You should see output containing openjdk version "17 followed by a version number.

Why JDK 17?

JDK 17 is a Long-Term Support (LTS) release. Spring Boot 3.x requires Java 17 as its minimum version. LTS releases receive security patches for years, making them the safest choice for production applications.

Install Maven

Maven is a build tool that manages your project's dependencies, compiles your code, and packages it into a runnable JAR. Spring Boot is compatible with Apache Maven 3.6.3 or later.

  • Check if Maven is already installed by running this command:
mvn -version

✔️ I see version 3.6.3 or higher

Your output shows something like Apache Maven 3.9.x. Maven is ready to go.

  • Skip ahead to the IntelliJ IDEA section below.

ⓧ I see an older version

Your Maven version is below 3.6.3. You need to upgrade.

macOS

  • Upgrade Maven using Homebrew by running:
brew upgrade maven
  • Verify the upgrade by running:
mvn -version

You should see Apache Maven 3.9 or higher in the output.

Windows

  • Download the latest Maven binary zip from the official Apache Maven website:

https://maven.apache.org

  • Extract the zip to a folder like C:\Program Files\Maven.
  • Add the bin folder inside the extracted directory to your system PATH environment variable.
  • Close and reopen PowerShell, then verify by running:
mvn -version

You should see Apache Maven 3.9 or higher in the output.

ⓧ Command not found

Maven is not installed on your machine.

macOS

  • Install Maven using Homebrew by running:
brew install maven
  • Verify the install by running:
mvn -version

You should see Apache Maven 3.9 or higher in the output.

Windows

  • Download the latest Maven binary zip from the official Apache Maven website:

https://maven.apache.org

  • Extract the zip to a folder like C:\Program Files\Maven.
  • Add the bin folder inside the extracted directory to your system PATH environment variable.
  • Close and reopen PowerShell, then verify by running:
mvn -version

You should see Apache Maven 3.9 or higher in the output.

Maven not found after installing?

Make sure you opened a fresh terminal window after installing Maven. On Windows, if you edited the PATH variable, you must close and reopen PowerShell for it to take effect.

Help me troubleshoot Maven installation issues.

Install IntelliJ IDEA Community Edition

IntelliJ IDEA is the most popular IDE for Java and Spring development. The free Community Edition has everything you need for this project.

  • Open the JetBrains download page in your browser:

https://www.jetbrains.com/idea/

  • Scroll down to find the Community Edition section (the free, open-source version).
  • Click Download for your operating system.
  • Run the downloaded installer and follow the prompts. Accept the defaults.
  • Open IntelliJ IDEA after installation. Press Cmd+Space (macOS) or the Windows key (Windows), type IntelliJ, and press Enter.

IntelliJ IDEA opens to a Welcome screen. You should see a notification that it has detected your installed JDK.

How does IntelliJ find my JDK?

IntelliJ IDEA automatically scans common installation paths for JDKs. If it does not detect your JDK 17 automatically, you can configure it manually later when you import the project.

IntelliJ not detecting JDK 17?

If IntelliJ does not show JDK 17 on the Welcome screen, do not worry. You will configure the project SDK when you import the Spring Boot project in the next step.

Help me configure JDK 17 in IntelliJ IDEA.

Your development environment is ready. Next up, you will generate a Spring Boot project using Spring Initializr and import it into IntelliJ.

Generate the Spring Boot Project

Your development environment is ready with JDK 17, Maven, and IntelliJ IDEA. Now you need a Spring Boot project to work with.

Instead of creating the project structure by hand, you will use Spring Initializr to generate a pre-configured project with all the dependencies wired up. This gives you a working skeleton in seconds so you can focus on writing code.

In this step, get ready to:
  • Generate a Spring Boot project with the correct dependencies using Spring Initializr.
  • Import the project into IntelliJ IDEA and resolve dependencies.
  • Verify the application starts successfully on port 8080.
Generate a project with Spring Initializr

Spring Initializr is a web tool that creates a ready-to-run Spring Boot project based on the options you select. You will configure it with the exact dependencies this project needs.

  • Open your browser and navigate to https://start.spring.io.
  • Under Project, select Maven.
  • Under Language, select Java.
  • Under Spring Boot, select 3.5.14.
  • In the Group field, enter com.example.
  • In the Artifact field, enter bookcatalog.
  • Click Add Dependencies.
  • Search for and add Spring Web.
  • Search for and add Spring Data JPA.
  • Search for and add H2 Database.
  • Search for and add Spring Boot Starter AOP.
  • Click Generate to download the project as a zip file.

What did you just configure?

Each dependency you added pulls in a Spring Boot starter, which is a pre-packaged bundle of libraries and auto-configuration.

  • Spring Web gives you an embedded Tomcat server and Spring MVC for building REST endpoints.
  • Spring Data JPA provides Hibernate for mapping Java objects to database tables.
  • H2 Database is an in-memory database that requires zero installation.
  • Spring Boot Starter AOP enables Spring AOP for adding cross-cutting concerns like logging.
Import the project into IntelliJ IDEA

The downloaded zip contains a complete Maven project structure. You need to extract it and open it in IntelliJ so it can download the dependencies and index the code.

  • Locate the downloaded bookcatalog.zip file in your Downloads folder and unzip it.
  • In IntelliJ IDEA, click File in the top menu bar.
  • Click Open.
  • Navigate to the unzipped bookcatalog folder and select it.
  • Click OK to open the project.
  • If prompted to trust the project, click Trust Project.
  • Wait for IntelliJ to finish importing and resolving dependencies. You will see a progress bar in the bottom-right corner while Maven downloads the libraries.

What is Maven doing?

Maven reads the pom.xml file in your project and downloads every library listed under <dependencies>. It stores them in a local cache on your machine so future builds are faster.

Dependencies not resolving?

Make sure you have an internet connection. If IntelliJ still shows red underlines, right-click the pom.xml file in the project explorer, then select Maven and Reload Project.

help me resolve Maven dependency errors in IntelliJ IDEA

Verify the project runs

Before writing any code, confirm the generated project starts cleanly. This catches configuration problems early.

  • Open IntelliJ's built-in terminal by clicking Terminal at the bottom of the window (or press Alt+F12 on Windows / Option+F12 on macOS).
  • Start the application by running this command:
./mvnw spring-boot:run

What does this command do?

The ./mvnw script is a Maven Wrapper included in the project. It ensures everyone uses the same Maven version without requiring a global install. The spring-boot:run goal compiles your code and starts the embedded Tomcat server.

  • Watch the terminal output. After a few seconds, you should see a line containing Tomcat started on port 8080.
  • Stop the application by pressing Ctrl+C in the terminal.

Seeing 'permission denied' on macOS?

The Maven Wrapper script may not have execute permissions. Run chmod +x mvnw in the terminal, then try ./mvnw spring-boot:run again.

help me fix permission errors when running the Maven wrapper

✔️ I see 'Tomcat started on port 8080'

Your Spring Boot project is generated, imported, and running. You are ready to start building the Book entity.

ⓧ I see an error in the terminal

Check that your JDK version is 17 or higher by running:

java -version

If you see a version lower than 17, revisit Step 1 to install the correct JDK. Also confirm that IntelliJ is using the correct JDK by going to File then Project Structure and checking the SDK setting.

help me fix Spring Boot startup errors related to JDK version

✔️ Awesome, I've got everything!

Great. Double check you stopped the application with Ctrl+C before moving on.

ⓧ I'd like to double check the full code

Here is what your pom.xml should look like after generation:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.5.14</version>
		<relativePath/>
	</parent>
	<groupId>com.example</groupId>
	<artifactId>bookcatalog</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>bookcatalog</name>
	<description>Book Catalog REST API</description>
	<properties>
		<java.version>17</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-aop</artifactId>
		</dependency>
		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

And here is the main application class:

package com.example.bookcatalog;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BookcatalogApplication {

	public static void main(String[] args) {
		SpringApplication.run(BookcatalogApplication.class, args);
	}
}

Your Spring Boot skeleton is alive and responding. Next up, you will create the Book entity class and connect it to an in-memory database using Hibernate.

Create the Book Entity with Hibernate

Your Spring Boot project compiles and runs, but it has no concept of what a "book" is yet. Before you can build an API, you need to define the data model that represents a book and tell the application how to store it.

In this step, you will create a Java class annotated with Hibernate JPA annotations that maps directly to a database table. You will also configure an H2 in-memory database and verify that Hibernate automatically creates the table from your class.

In this step, get ready to:
  • Create a Book entity class with JPA annotations.
  • Configure H2 database and Hibernate settings.
  • Verify the BOOKS table exists in the H2 console.
Create the Book entity class

Hibernate needs a Java class annotated as an entity to know which database table to create. Each field in the class becomes a column in that table.

  • In IntelliJ's Project panel on the left, expand src/main/java/com/example/bookcatalog.
  • Right-click on the bookcatalog package.
  • Select New, then Package.
  • Type model and press Enter.
  • Right-click the new model package.
  • Select New, then Java Class.
  • Name it Book and press Enter.
  • Replace the entire generated file content with the following code:
package com.example.bookcatalog.model;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

// Marks this class as a JPA entity mapped to a database table
@Entity
@Table(name = "books")
public class Book {

    // Primary key with auto-incrementing ID
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;
    private String author;
    private String isbn;
    private Integer publishedYear;

    // JPA requires a no-args constructor
    public Book() {
    }

    // Convenience constructor for creating Book instances
    public Book(String title, String author, String isbn, Integer publishedYear) {
        this.title = title;
        this.author = author;
        this.isbn = isbn;
        this.publishedYear = publishedYear;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public String getIsbn() {
        return isbn;
    }

    public void setIsbn(String isbn) {
        this.isbn = isbn;
    }

    public Integer getPublishedYear() {
        return publishedYear;
    }

    public void setPublishedYear(Integer publishedYear) {
        this.publishedYear = publishedYear;
    }
}

What does this code do?

This class defines the shape of a book in your application and maps it to a database table.

  • @Entity tells JPA that this class represents a row in a database table.
  • @Table(name = "books") sets the actual table name in the database to books.
  • @Id marks id as the primary key. @GeneratedValue(strategy = GenerationType.IDENTITY) tells the database to auto-increment it.
  • The no-args constructor is required by JPA. The parameterized constructor is a convenience for creating books in code.
  • Save Book.java with Cmd+S (macOS) or Ctrl+S (Windows).

Seeing red underlines on the imports?

If IntelliJ shows errors on the jakarta.persistence imports, Maven may not have finished downloading dependencies. Click the Load Maven Changes icon that appears in the top-right of the editor, or right-click pom.xml and select Maven then Reload Project.

help me fix red import errors in my Spring Boot entity class

Configure H2 and JPA settings

Spring Boot needs to know where to store data and how to manage the database schema. You will configure an in-memory H2 database and tell Hibernate to automatically create tables from your entity classes.

  • In IntelliJ's Project panel, expand src/main/resources.
  • Double-click application.properties to open it.
  • Replace the entire file content with the following configuration:
spring.application.name=bookcatalog

# H2 Database Configuration
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

# JPA / Hibernate
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

# H2 Console
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

What do these properties do?

  • spring.datasource.url=jdbc:h2:mem:testdb creates an in-memory database named testdb. Data lives only while the app runs.
  • spring.jpa.hibernate.ddl-auto=create-drop tells Hibernate to create all tables on startup and drop them on shutdown. This is ideal for development.
  • spring.jpa.show-sql=true prints every SQL statement Hibernate executes to the console, so you can see exactly what it does.
  • spring.h2.console.enabled=true activates the H2 web console, a browser-based tool for inspecting your database.
  • Save application.properties.

Not sure if the file saved correctly?

Check that there are no extra spaces or invisible characters in property values. The spring.datasource.password= line must have nothing after the equals sign (the password is intentionally blank for H2).

help me verify my application.properties is correct

Verify the entity in the H2 console

Now you will run the application and confirm that Hibernate created the BOOKS table from your @Entity annotations.

  • Start the application by running this command in the IntelliJ terminal:
./mvnw spring-boot:run
  • Wait until you see Tomcat started on port 8080 in the console output.
  • Open your browser and navigate to http://localhost:8080/h2-console.

What should I see?

You should see the H2 Console login page with a JDBC URL field. The default value might not match your configuration.

  • In the JDBC URL field, replace any existing value with jdbc:h2:mem:testdb.
  • Leave User Name as sa and Password empty.
  • Click Connect.
  • In the left panel, you should see the BOOKS table listed. This confirms Hibernate read your @Entity class and created the corresponding table automatically.

Why is this significant?

You did not write any SQL to create this table. Hibernate scanned your Book class, saw the @Entity and @Table annotations, and generated the CREATE TABLE SQL for you. This is Object-Relational Mapping (ORM) in action.

  • Return to your terminal and press Ctrl+C to stop the application.

Don't see the BOOKS table?

Make sure the JDBC URL in the H2 console login is exactly jdbc:h2:mem:testdb. If you use a different URL, you are connecting to a different (empty) database.

Also check your terminal output for error messages. If you see a stack trace mentioning Book or Entity, the class may have a typo in the annotations or a missing import.

help me fix my H2 console or Book entity issue

✔️ Awesome, I've got everything!

Great. Double check you have saved both Book.java and application.properties.

ⓧ I'd like to double check the full code

Here is the full code for both files you created or edited in this step.

package com.example.bookcatalog.model;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "books")
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;
    private String author;
    private String isbn;
    private Integer publishedYear;

    public Book() {
    }

    public Book(String title, String author, String isbn, Integer publishedYear) {
        this.title = title;
        this.author = author;
        this.isbn = isbn;
        this.publishedYear = publishedYear;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public String getIsbn() {
        return isbn;
    }

    public void setIsbn(String isbn) {
        this.isbn = isbn;
    }

    public Integer getPublishedYear() {
        return publishedYear;
    }

    public void setPublishedYear(Integer publishedYear) {
        this.publishedYear = publishedYear;
    }
}
spring.application.name=bookcatalog

# H2 Database Configuration
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

# JPA / Hibernate
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

# H2 Console
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

Your data model is defined and the database is wired up. Next, you will build the repository and service layers that let the application read and write books to this table.

Build the Repository and Service Layers

You have a Book entity mapped to a database table. Now you need a way for your application to talk to that table and perform operations like saving, finding, and deleting books.

In Spring's layered architecture, the Repository handles database access and the Service contains your business logic. Spring creates and wires these components together automatically using dependency injection. You never call new yourself.

In this step, get ready to:
  • Create a Spring Data JPA repository interface that auto-generates database queries.
  • Build a service class with full CRUD operations using constructor injection.
  • Observe how Spring Beans and dependency injection eliminate manual object creation.
Create the BookRepository interface

Spring Data JPA lets you define a repository as a plain Java interface. You declare method signatures that follow a naming convention, and Spring auto-generates the SQL implementation at runtime.

  • In IntelliJ, right-click the com.example.bookcatalog package in the src/main/java directory.
  • Select New then Package.
  • Enter com.example.bookcatalog.repository and press Enter.
  • Right-click the new repository package, select New then Java Class.
  • Choose Interface from the kind dropdown.
  • Name it BookRepository and press Enter.
  • Replace the file contents with the following code:
package com.example.bookcatalog.repository;

import com.example.bookcatalog.model.Book;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface BookRepository extends JpaRepository<Book, Long> {

    // Spring Data JPA auto-generates a query from this method name
    List<Book> findByAuthor(String author);
}

What does this code do?

By extending JpaRepository<Book, Long>, your interface automatically inherits methods like findAll(), findById(), save(), and deleteById(). You never write the implementation.

The custom method findByAuthor(String author) uses Spring Data's query derivation. Spring parses the method name, sees findBy + Author, and generates a SELECT * FROM books WHERE author = ? query automatically.

  • Save BookRepository.java.

Why is there no class implementing this interface?

Spring Data JPA creates a proxy implementation at runtime. When your application starts, Spring scans for interfaces extending JpaRepository and generates a fully functional bean with all the CRUD methods built in.

Create the BookService class

The service layer sits between the controller (which handles HTTP) and the repository (which handles the database). It contains your business logic and coordinates operations.

Before creating the service, you need a marker annotation called @Loggable that the service methods will use. You will wire this annotation to an AOP logging aspect in a later step.

  • Right-click the com.example.bookcatalog package, select New then Package.
  • Enter com.example.bookcatalog.aspect and press Enter.
  • Right-click the aspect package, select New then Java Class.
  • Choose Annotation from the kind dropdown.
  • Name it Loggable and press Enter.
  • Replace the file contents with the following code:
package com.example.bookcatalog.aspect;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// Marker annotation for methods that should be logged by AOP
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
}

What does this annotation do?

This is a custom marker annotation. On its own, it does nothing. In a later step, you will create an AOP aspect that intercepts any method marked with @Loggable and automatically logs its execution time and arguments.

The @Retention(RetentionPolicy.RUNTIME) ensures the annotation is available at runtime so the aspect can detect it. The @Target restricts where it can be placed (methods and types).

  • Save Loggable.java.

Now create the service class that uses the repository.

  • Right-click the com.example.bookcatalog package, select New then Package.
  • Enter com.example.bookcatalog.service and press Enter.
  • Right-click the service package, select New then Java Class.
  • Name it BookService and press Enter.
  • Replace the file contents with the following code:
package com.example.bookcatalog.service;

import com.example.bookcatalog.aspect.Loggable;
import com.example.bookcatalog.model.Book;
import com.example.bookcatalog.repository.BookRepository;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service
public class BookService {

    private final BookRepository bookRepository;

    // Constructor injection - Spring automatically provides the BookRepository bean
    public BookService(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }

    @Loggable
    public List<Book> getAllBooks() {
        return bookRepository.findAll();
    }

    @Loggable
    public Optional<Book> getBookById(Long id) {
        return bookRepository.findById(id);
    }

    @Loggable
    public Book createBook(Book book) {
        return bookRepository.save(book);
    }

    @Loggable
    public Book updateBook(Long id, Book bookDetails) {
        Book book = bookRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("Book not found with id: " + id));
        book.setTitle(bookDetails.getTitle());
        book.setAuthor(bookDetails.getAuthor());
        book.setIsbn(bookDetails.getIsbn());
        book.setPublishedYear(bookDetails.getPublishedYear());
        return bookRepository.save(book);
    }

    @Loggable
    public void deleteBook(Long id) {
        bookRepository.deleteById(id);
    }

    @Loggable
    public List<Book> getBooksByAuthor(String author) {
        return bookRepository.findByAuthor(author);
    }
}

What does this code do?

The @Service annotation tells Spring to register this class as a Spring Bean in the application context. Spring manages its lifecycle and makes it available for injection into other components.

The constructor accepts a BookRepository parameter. Spring sees this dependency and automatically injects the repository bean it created from your interface. This is constructor-based dependency injection.

Each method delegates to the repository. The updateBook method first fetches the existing book, updates its fields, and saves it back. The @Loggable annotation marks every method for automatic logging once the AOP aspect is wired up.

  • Save BookService.java.
  • Open a terminal in the project directory and verify the application compiles by running:
./mvnw compile
  • You should see BUILD SUCCESS at the end of the output.

Seeing a compilation error?

  • Check that Loggable.java is in the com.example.bookcatalog.aspect package. The import in BookService.java must match the actual package path.
  • Verify your BookRepository.java is in com.example.bookcatalog.repository. A misplaced file means Spring cannot find it.
  • Make sure Book.java from the previous step is saved and in com.example.bookcatalog.model.

Help me fix a compilation error in my BookService or BookRepository

Understand the Spring Bean lifecycle

Take a moment to notice something important about the code you just wrote.

Where is the new keyword?

You never wrote new BookRepository() or new BookService(). Spring's Inversion of Control (IoC) container handles all object creation.

When your application starts, Spring scans for classes annotated with @Service, @Component, @Repository, etc. It creates a single instance (a bean) of each and stores them in the application context. When BookService needs a BookRepository, Spring looks up the matching bean and injects it through the constructor.

This is the core of Spring Bean management. Your code declares what it needs. Spring figures out how to provide it.

✔️ Awesome, I've got everything!

Great. Double check you have saved all three new files before moving on.

ⓧ I'd like to double check the full code

Here is BookRepository.java:

package com.example.bookcatalog.repository;

import com.example.bookcatalog.model.Book;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface BookRepository extends JpaRepository<Book, Long> {

    List<Book> findByAuthor(String author);
}

Here is Loggable.java:

package com.example.bookcatalog.aspect;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
}

Here is BookService.java:

package com.example.bookcatalog.service;

import com.example.bookcatalog.aspect.Loggable;
import com.example.bookcatalog.model.Book;
import com.example.bookcatalog.repository.BookRepository;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service
public class BookService {

    private final BookRepository bookRepository;

    public BookService(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }

    @Loggable
    public List<Book> getAllBooks() {
        return bookRepository.findAll();
    }

    @Loggable
    public Optional<Book> getBookById(Long id) {
        return bookRepository.findById(id);
    }

    @Loggable
    public Book createBook(Book book) {
        return bookRepository.save(book);
    }

    @Loggable
    public Book updateBook(Long id, Book bookDetails) {
        Book book = bookRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("Book not found with id: " + id));
        book.setTitle(bookDetails.getTitle());
        book.setAuthor(bookDetails.getAuthor());
        book.setIsbn(bookDetails.getIsbn());
        book.setPublishedYear(bookDetails.getPublishedYear());
        return bookRepository.save(book);
    }

    @Loggable
    public void deleteBook(Long id) {
        bookRepository.deleteById(id);
    }

    @Loggable
    public List<Book> getBooksByAuthor(String author) {
        return bookRepository.findByAuthor(author);
    }
}

Your repository and service layers are ready. Next up, you will create a REST controller that exposes these operations as HTTP endpoints anyone can call.

Create the REST Controller with Spring MVC

Your repository and service layers are ready to manage book data behind the scenes. Now you need a way for the outside world to interact with your application over HTTP.

Spring MVC provides annotations that turn a plain Java class into a REST API. In this step, you will create a controller that maps HTTP requests to the service methods you built in the previous step.

In this step, get ready to:
  • Create the BookController class with constructor injection.
  • Implement six CRUD endpoints using Spring MVC annotations.
  • Test the API with curl commands and see JSON responses.
Create the BookController class

The controller is the front door of your API. It receives HTTP requests, delegates work to the service layer, and returns responses. @RestController combines @Controller and @ResponseBody so every method automatically serializes its return value to JSON.

  • In the IntelliJ Project panel on the left, right-click src/main/java/com/example/bookcatalog.
  • Select New then Package.
  • Enter controller as the package name and press Enter.
  • Right-click the new controller package, select New then Java Class.
  • Name it BookController and press Enter.
  • Replace the generated file contents with the following code:
package com.example.bookcatalog.controller;

import com.example.bookcatalog.aspect.Loggable;
import com.example.bookcatalog.model.Book;
import com.example.bookcatalog.service.BookService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

// Marks this class as a REST controller — all methods return JSON by default
@RestController
// Every endpoint in this class starts with /api/books
@RequestMapping("/api/books")
public class BookController {

    // Spring injects the BookService bean here via constructor injection
    private final BookService bookService;

    public BookController(BookService bookService) {
        this.bookService = bookService;
    }
}

What does this code do?

@RestController tells Spring this class handles HTTP requests and automatically converts return values to JSON. @RequestMapping("/api/books") sets a base URL path so all endpoints share the /api/books prefix.

The constructor accepts a BookService parameter. Spring sees there is only one constructor and automatically injects the BookService bean. This is the same constructor injection pattern you used in BookService itself.

Notice that Spring manages the entire chain: it creates BookRepository, injects it into BookService, then injects BookService into BookController. You never call new yourself.

You will not see a change yet. The next chunk adds the endpoint methods that make this controller useful.

Implement the read and create endpoints
  • Inside the BookController class, add the following three methods after the constructor's closing brace:
    // GET /api/books — returns all books as a JSON array
    @GetMapping
    @Loggable
    public List<Book> getAllBooks() {
        return bookService.getAllBooks();
    }

    // GET /api/books/{id} — returns a single book or 404 if not found
    @GetMapping("/{id}")
    @Loggable
    public ResponseEntity<Book> getBookById(@PathVariable Long id) {
        return bookService.getBookById(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }

    // POST /api/books — creates a new book and returns 201 Created
    @PostMapping
    @Loggable
    public ResponseEntity<Book> createBook(@RequestBody Book book) {
        Book createdBook = bookService.createBook(book);
        return ResponseEntity.status(HttpStatus.CREATED).body(createdBook);
    }

What do these annotations do?

@GetMapping maps HTTP GET requests to a method. @PostMapping maps POST requests. Combined with the class-level @RequestMapping("/api/books"), they resolve to the full URL paths.

@PathVariable extracts {id} from the URL. @RequestBody tells Spring to deserialize the incoming JSON body into a Book object. ResponseEntity lets you control the HTTP status code alongside the response body.

You will not see a change yet. The next chunk completes the remaining endpoints.

Implement the update, delete, and search endpoints
  • Below the createBook method, add the final three endpoint methods:
    // PUT /api/books/{id} — updates an existing book's fields
    @PutMapping("/{id}")
    @Loggable
    public ResponseEntity<Book> updateBook(@PathVariable Long id, @RequestBody Book bookDetails) {
        Book updatedBook = bookService.updateBook(id, bookDetails);
        return ResponseEntity.ok(updatedBook);
    }

    // DELETE /api/books/{id} — removes a book and returns 204 No Content
    @DeleteMapping("/{id}")
    @Loggable
    public ResponseEntity<Void> deleteBook(@PathVariable Long id) {
        bookService.deleteBook(id);
        return ResponseEntity.noContent().build();
    }

    // GET /api/books/search?author=Name — finds books by a specific author
    @GetMapping("/search")
    @Loggable
    public List<Book> getBooksByAuthor(@RequestParam String author) {
        return bookService.getBooksByAuthor(author);
    }

What does this code do?

@PutMapping handles PUT requests for updates. @DeleteMapping handles DELETE requests. Each maps directly to the corresponding service method you already wrote.

@RequestParam in getBooksByAuthor reads the author value from the query string (e.g. /api/books/search?author=Craig). The controller delegates to BookService.getBooksByAuthor() which calls your custom repository method.

  • Save BookController.java with Cmd+S (macOS) or Ctrl+S (Windows).
  • Check IntelliJ's editor for any red underlines. If the file compiles without errors, you are ready to run.

Seeing red underlines on imports?

Make sure Loggable.java exists in the com.example.bookcatalog.aspect package from the previous step. The controller imports @Loggable from that package.

Also confirm your package declaration says package com.example.bookcatalog.controller; at the top of the file.

Help me fix import errors in my BookController

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

package com.example.bookcatalog.controller;

import com.example.bookcatalog.aspect.Loggable;
import com.example.bookcatalog.model.Book;
import com.example.bookcatalog.service.BookService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/books")
public class BookController {

    private final BookService bookService;

    public BookController(BookService bookService) {
        this.bookService = bookService;
    }

    @GetMapping
    @Loggable
    public List<Book> getAllBooks() {
        return bookService.getAllBooks();
    }

    @GetMapping("/{id}")
    @Loggable
    public ResponseEntity<Book> getBookById(@PathVariable Long id) {
        return bookService.getBookById(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }

    @PostMapping
    @Loggable
    public ResponseEntity<Book> createBook(@RequestBody Book book) {
        Book createdBook = bookService.createBook(book);
        return ResponseEntity.status(HttpStatus.CREATED).body(createdBook);
    }

    @PutMapping("/{id}")
    @Loggable
    public ResponseEntity<Book> updateBook(@PathVariable Long id, @RequestBody Book bookDetails) {
        Book updatedBook = bookService.updateBook(id, bookDetails);
        return ResponseEntity.ok(updatedBook);
    }

    @DeleteMapping("/{id}")
    @Loggable
    public ResponseEntity<Void> deleteBook(@PathVariable Long id) {
        bookService.deleteBook(id);
        return ResponseEntity.noContent().build();
    }

    @GetMapping("/search")
    @Loggable
    public List<Book> getBooksByAuthor(@RequestParam String author) {
        return bookService.getBooksByAuthor(author);
    }
}
Test the API with curl

Your controller is complete. Time to start the application and send real HTTP requests to it.

  • Open a terminal in the project root directory (the bookcatalog folder that contains pom.xml).
  • Start the application by running:
./mvnw spring-boot:run
  • Wait until you see Tomcat started on port 8080 in the console output.
  • Open a second terminal tab or window. Create a book by running this curl command:
curl -X POST http://localhost:8080/api/books -H "Content-Type: application/json" -d '{"title":"Spring in Action","author":"Craig Walls","isbn":"978-1617297571","publishedYear":2022}'

What does this command do?

curl -X POST sends an HTTP POST request. The -H flag sets the Content-Type header to JSON so Spring knows to parse the body. The -d flag provides the JSON body containing book data.

Spring deserializes this JSON into a Book object, passes it to createBook(), and Hibernate saves it to the H2 database. The response comes back with the saved book (including its generated id) and a 201 status code.

  • Retrieve all books by running:
curl http://localhost:8080/api/books
  • You should see a JSON array containing the book you just created, similar to:
[{"id":1,"title":"Spring in Action","author":"Craig Walls","isbn":"978-1617297571","publishedYear":2022}]

What is Spring MVC?

Spring MVC is the web framework inside Spring that routes HTTP requests to Java methods. Each annotation (@GetMapping, @PostMapping, etc.) tells Spring which HTTP method and URL path trigger which Java method.

When you return an object from a @RestController method, Spring automatically serializes it to JSON using Jackson (included by spring-boot-starter-web). You never write JSON serialization code yourself.

Getting "Connection refused" from curl?

Make sure the application is still running in your first terminal. Look for Tomcat started on port 8080 in the output.

If you see a compilation error instead of the startup message, check that BookController.java is saved and that all imports resolve correctly.

Help me troubleshoot my Spring Boot application not starting

Your REST API is live and returning data. In the next step, you will add cross-cutting logging with Spring AOP so every controller and service method is automatically traced without touching business code.

Add Cross-Cutting Logging with Spring AOP

Your API is fully functional, but imagine you need to track how long each method takes or which arguments are passed to every endpoint. Adding logging statements manually to every method creates repetitive code that clutters your business logic.

This is exactly the problem Aspect-Oriented Programming solves. In this step, you will create a custom annotation and an aspect that automatically logs method entry, exit, execution time, and arguments for any method you annotate.

In this step, get ready to:
  • Create a custom @Loggable annotation that marks methods for automatic logging.
  • Build a LoggingAspect class that intercepts annotated methods and logs their execution details.
  • Verify that AOP logging appears in the console without modifying any business code.
Create the custom @Loggable annotation

Your BookController and BookService already reference @Loggable on their methods, but the annotation does not exist yet. You need to create it so the project compiles.

  • In IntelliJ, right-click the com.example.bookcatalog package in the Project panel.
  • Select New then Package.
  • Name the new package aspect and press Enter.
  • Right-click the new aspect package, select New then Java Class.
  • Choose Annotation from the dropdown and name it Loggable.
  • Replace the generated content with the following code:
package com.example.bookcatalog.aspect;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// Custom annotation to mark methods for automatic logging
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
}

What does this code do?

This defines a custom Java annotation called @Loggable. Annotations are metadata markers you attach to code.

  • @Target({ElementType.METHOD, ElementType.TYPE}) means you can place this annotation on individual methods or on entire classes.
  • @Retention(RetentionPolicy.RUNTIME) means the annotation survives compilation and is available at runtime, which is essential for Spring AOP to detect it.
  • Save Loggable.java.
  • Check that IntelliJ no longer shows red underlines on the @Loggable references in BookController.java and BookService.java.

Still seeing red underlines?

Make sure the package declaration at the top of Loggable.java reads package com.example.bookcatalog.aspect; exactly. The import in BookController expects com.example.bookcatalog.aspect.Loggable.

If the error persists, click File then Invalidate Caches / Restart to force IntelliJ to re-index.

Help me fix compilation errors with my @Loggable annotation

Create the LoggingAspect class

The annotation by itself does nothing. You need an aspect class that Spring detects at runtime, intercepts methods carrying @Loggable, and wraps them with logging logic.

  • Right-click the aspect package, select New then Java Class.
  • Name the class LoggingAspect and press Enter.
  • Replace the generated content with the following code:
package com.example.bookcatalog.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.Arrays;

// Spring AOP aspect that logs method execution details
@Aspect
@Component
public class LoggingAspect {

    // SLF4J logger for this aspect class
    private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);

    // Intercept any method annotated with @Loggable
    @Around("@annotation(com.example.bookcatalog.aspect.Loggable)")
    public Object logMethodExecution(ProceedingJoinPoint joinPoint) throws Throwable {
        // Capture method name and arguments before execution
        String methodName = joinPoint.getSignature().toShortString();
        Object[] args = joinPoint.getArgs();

        logger.info(">>> Entering: {} with arguments: {}", methodName, Arrays.toString(args));

        long startTime = System.currentTimeMillis();

        Object result;
        try {
            // Execute the original method
            result = joinPoint.proceed();
        } catch (Throwable throwable) {
            // Log any exception that occurs during execution
            logger.error("!!! Exception in {}: {}", methodName, throwable.getMessage());
            throw throwable;
        }

        // Calculate and log execution duration
        long duration = System.currentTimeMillis() - startTime;
        logger.info("<<< Exiting: {} | Duration: {}ms | Result: {}", methodName, duration, result);

        return result;
    }
}

What does this code do?

This is the core of Spring AOP in action. The @Aspect annotation marks this class as an aspect, and @Component registers it as a Spring Bean so the container manages it.

  • @Around("@annotation(com.example.bookcatalog.aspect.Loggable)") is the pointcut expression. It tells Spring to wrap any method carrying @Loggable with this advice.
  • ProceedingJoinPoint represents the intercepted method. Calling joinPoint.proceed() runs the original method.
  • The aspect captures the method name, arguments, execution time, and return value without touching any business code in the controller or service.

Why a custom annotation instead of package-wide pointcuts?

You could write a pointcut that matches every method in a package, but a custom annotation makes the behaviour explicit. When you see @Loggable on a method, you know immediately that it is being logged.

This opt-in pattern is also a common real-world approach. Production applications use custom annotations to selectively apply caching, security checks, and metrics collection.

  • Save LoggingAspect.java.
  • Confirm IntelliJ shows no compilation errors in the file.

Seeing unresolved import errors?

If org.aspectj.lang is not found, confirm that spring-boot-starter-aop is in your pom.xml dependencies. Then click the Maven panel on the right side of IntelliJ and click the refresh (reload) icon to re-import dependencies.

Help me resolve missing AspectJ imports in my LoggingAspect

✔️ Awesome, I've got everything!

Great. Double check you have saved both files before moving on.

ⓧ I'd like to double check the full code

Here is the complete Loggable.java:

package com.example.bookcatalog.aspect;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
}

Here is the complete LoggingAspect.java:

package com.example.bookcatalog.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.Arrays;

@Aspect
@Component
public class LoggingAspect {

    private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);

    @Around("@annotation(com.example.bookcatalog.aspect.Loggable)")
    public Object logMethodExecution(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().toShortString();
        Object[] args = joinPoint.getArgs();

        logger.info(">>> Entering: {} with arguments: {}", methodName, Arrays.toString(args));

        long startTime = System.currentTimeMillis();

        Object result;
        try {
            result = joinPoint.proceed();
        } catch (Throwable throwable) {
            logger.error("!!! Exception in {}: {}", methodName, throwable.getMessage());
            throw throwable;
        }

        long duration = System.currentTimeMillis() - startTime;
        logger.info("<<< Exiting: {} | Duration: {}ms | Result: {}", methodName, duration, result);

        return result;
    }
}
Verify AOP in action

Time to see the aspect intercepting real method calls. You will start the application and make a request to trigger the logging.

  • Open a terminal in IntelliJ by clicking View then Tool Windows then Terminal.
  • Start the application by running:
./mvnw spring-boot:run
  • Wait until you see Tomcat started on port 8080 in the console output.
  • Open a second terminal tab and make a request by running:
curl http://localhost:8080/api/books
  • Switch back to the first terminal tab (where the app is running).
  • Look at the console output. You should see log lines containing >>> Entering: and <<< Exiting: with the method name, arguments, duration, and result.

What should I see?

The console output shows both the controller and service methods being logged because both carry @Loggable. You will see entries like:

  • >>> Entering: BookController.getAllBooks() with arguments: [] when the request hits the controller.
  • >>> Entering: BookService.getAllBooks() with arguments: [] when the controller calls the service.
  • <<< Exiting: BookService.getAllBooks() | Duration: Xms | Result: [] when the service finishes.

The aspect runs automatically on every annotated method. You did not add a single logger.info() call to your business code.

Extra for Experts: Why don't internal method calls get logged?

Spring AOP is proxy-based. Only calls that come through the Spring proxy (external calls from one bean to another) are intercepted.

If BookService.updateBook() called this.getBookById() internally, the @Loggable on getBookById would NOT trigger because the call goes through this instead of through the proxy. This is a fundamental characteristic of proxy-based AOP to keep in mind.

Not seeing any log lines with >>> or <<<?

Make sure the LoggingAspect class has both @Aspect and @Component annotations. Without @Component, Spring does not register the class as a bean and the aspect never activates.

Also confirm you restarted the application after saving both files. Changes only take effect after a fresh start.

Help me debug why my Spring AOP logging aspect is not working

Secret mission

Add Pagination and Sorting

Real-world APIs rarely return every record at once. Add a paginated search endpoint that demonstrates Spring Data's powerful Pageable interface, giving clients control over page size, page number, and sort order.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally with an in-memory H2 database, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • The running Spring Boot application (process on port 8080).
  • The bookcatalog project folder on your Desktop.
  • IntelliJ IDEA Community Edition (installed application).
  • JDK 17 (installed runtime).
  • Maven (installed build tool).

Keep everything running

No action needed. Choose this if you are still actively building or plan to extend the API with more endpoints.

  • Your Spring Boot application continues running on port 8080 until you stop it.
  • Your project folder, JDK, Maven, and IntelliJ IDEA all remain on your machine ready for future Java projects.

Pause - I'll come back to this later

Shut down the running application to free up memory and port 8080. Your project files stay on disk so you can pick up where you left off.

  • Switch to the terminal where the Spring Boot application is running.
  • Press Ctrl+C to stop the process.
  • To restart later, open a terminal in the bookcatalog folder and run ./mvnw spring-boot:run.

Delete - I don't want to use this again

Remove all project resources and optionally uninstall the tools you set up.

  • Switch to the terminal where the Spring Boot application is running and press Ctrl+C to stop it.
  • Delete the project folder by running the following command.

macOS

rm -rf ~/Desktop/bookcatalog

Windows

rmdir /s /q %USERPROFILE%\Desktop\bookcatalog
  • To remove IntelliJ IDEA, drag it to the Trash on macOS or use Add or remove programs on Windows.
  • To remove Maven installed via Homebrew, run brew uninstall maven. If you installed it manually, delete the Maven directory you extracted.
  • To remove JDK 17 installed via SDKMAN, run sdk uninstall java 17.0.13-librca. If you installed it manually, delete the JDK directory from your system.

Nice Work!

Nice Work!

You just built a complete Book Catalog REST API with Spring Boot. Your application demonstrates how the core Spring concepts work together in a real layered architecture.

You've learned how to:

  • Build a layered Spring Boot application with a Controller, Service, and Repository architecture wired together using constructor injection of Spring Beans.
  • Map a Java entity to a database table using Hibernate JPA annotations and auto-generate queries with Spring Data JPA repositories.
  • Expose full CRUD REST endpoints using Spring MVC and add automatic cross-cutting logging with Spring AOP and a custom annotation.
  • Added pagination and sorting with Spring Data's Pageable interface.

Ready to quiz yourself?