Spring Boot Rest API with MyBatis and PostgreSQL

In this post, we’ll implement a sample REST API that uses Spring Boot with MyBatis and PostgreSQL.


· Prerequisites
· Overview
∘ What is MyBatis?
∘ Why Mybatis?
· Getting Started
∘ Project Structure
∘ Database initialization
· Spring MyBatis integration
∘ Create Book Model
∘ Create Book Repository
∘ Service Layer
∘ Create Book Controller
· Test the application
· Conclusion
· References


Prerequisites

This is the list of all the prerequisites:

  • Spring Boot 4 or later
  • Maven 3.9.12 or later
  • Java 21
  • Liquibase for schema versioning
  • Your favorite IDE (IntelliJ IDEA, Eclipse, or VS Code)
  • Postman / insomnia or any other API testing tool.
  • PostgreSQL 14 or higher

Overview

What is MyBatis?

MyBatis is a first-class persistence framework with support for custom SQL, stored procedures, and advanced mappings. MyBatis eliminates almost all of the JDBC code and manual setting of parameters and retrieval of results. MyBatis can use simple XML or Annotations for configuration and map primitives, Map interfaces, and Java POJOs (Plain Old Java Objects) to database records.

Why Mybatis?

MyBatis comes with the following design philosophies −

  • Open source− MyBatis is free and open-source software.
  • Simplicity − MyBatis is widely regarded as one of the simplest persistence frameworks available today.
  • Fast Development − MyBatis does all it can to facilitate hyper-fast development.
  • Performance: Fine-tune queries for complex operations and joins
  • Independent Interfaces − MyBatis provides database-independent interfaces, and APIs that help the rest of the application remain independent of any persistence-related resources.
  • SQL Control: Write and optimize your own SQL queries rather than relying on auto-generated ones
  • Inline SQL − No pre-compiler is needed, and you can have full access to all of the features of SQL.
  • Dynamic SQL − MyBatis provides features for dynamic building SQL queries based on parameters.

Getting Started

We will start by creating a simple Spring Boot project from start.spring.io, with the following dependencies: Spring Boot, Liquibase, Mybatis, Lombok, Postgresql, and Validation.

The pom.xml should look something like this:

 <dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-liquibase</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>

<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>4.0.1</version>
</dependency>

<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-liquibase-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter-test</artifactId>
<version>4.0.1</version>
<scope>test</scope>
</dependency>
</dependencies>

Project Structure

Our project structure will look like this:

.
├── pom.xml
├── README.md
└── src
├── main
│ ├── java
│ │ └── com.example.springbootmybatispostgres
│ │ ├── common
│ │ │ └── exception
│ │ ├── entities
│ │ ├── repositories
│ │ ├── service
│ │ ├── web
│ │ └── SpringBootMyBatisPostgresApplication.java
│ └── resources
│ ├── application.yml
│ └── db
│ └── changelog
└── test

Database initialization

We added liquibase configurations to create the database schema and insert data before use by MyBatis. We’ll create the changelog versions in the /resources/db/changelog

Creating the book table (001_create_book_table.sql)


CREATE SEQUENCE book_id_seq;

CREATE TABLE book(
id bigint PRIMARY KEY DEFAULT nextval('book_id_seq'),
title VARCHAR(100) not null,
isbn VARCHAR(255),
description VARCHAR(255),
page int,
price numeric
);

ALTER SEQUENCE book_id_seq
OWNED BY book.id;

We will create a new changelog YAML file (db.changelog-1.0.yaml) that runs SQL scripts.

databaseChangeLog:

- changeSet:
id: "1.0"
author: boottech

- include:
file: book/001_create_book_table.sql
relativeToChangelogFile: true

- include:
file: author/001_create_author_table.sql
relativeToChangelogFile: true

- changeSet:
id: 202207011812-1-data
author: boottech
changes:
- loadData:
catalogName: cat
columns:
- column:
header: title
name: title
type: string
- column:
header: page
name: page
type: numeric
- column:
header: isbn
name: isbn
type: string
- column:
header: description
name: description
type: string
- column:
header: price
name: price
type: numeric
commentLineStartsWith: //
encoding: UTF-8
file: book/book.csv
quotchar: '""'
relativeToChangelogFile: true
schemaName: public
separator: ','
tableName: book
usePreparedStatements: true

Spring MyBatis integration

After database initialization, we need to implement the codes that interact between spring and MyBatis.

Create Book Model

Let’s start with our base domain entity.

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Book {

private Long id;

@NotBlank(message = "Title is required")
@Size(max = 100, message = "Title must not exceed 100 characters")
private String title;

@Min(value = 1, message = "Page count must be at least 1")
private int page;

@Size(max = 20, message = "ISBN must not exceed 20 characters")
private String isbn;

private String description;

@DecimalMin(value = "0.0", inclusive = false, message = "Price must be greater than 0")
private double price;
}

Unlike JPA entities, MyBatis models don’t need annotations like @Entity or @Table. They are just plain Java objects.

Create Book Repository

MyBatis-Spring-Boot-Starter will:

  • Autodetect an existing DataSource
  • Will create and register an instance of a SqlSessionFactory passing that DataSource as an input using the SqlSessionFactoryBean
  • Will create and register an instance of a SqlSessionTemplate got out of the SqlSessionFactory
  • Auto-scan your mappers, link them to the SqlSessionTemplate and register them to Spring context so they can be injected into your beans

The MyBatis-Spring-Boot-Starter will search, by default, for mappers marked with the @Mapper annotation.

@Mapper
@Repository
public interface BookRepository {

String SELECT_FROM_BOOK_WHERE_ID = "SELECT * FROM book WHERE id = #{id}";
String SELECT_FROM_BOOK = "SELECT * FROM book";

@Select(SELECT_FROM_BOOK)
List<Book> findAll();

@Select(SELECT_FROM_BOOK_WHERE_ID)
Book findById(long id);

@Select("SELECT * FROM book WHERE title = #{title}")
Book findByTitle(String title);

@Delete("DELETE FROM book WHERE id = #{id}")
int deleteById(long id);

@Insert("""
INSERT INTO book(title, isbn, description, page, price)
VALUES (#{title}, #{isbn}, #{description}, #{page}, #{price})
""")
void insert(Book book);

@Update("""
UPDATE book
SET title = #{title},
isbn = #{isbn},
description = #{description},
page = #{page},
price = #{price}
WHERE id = #{id}
""")
int update(Book book);
}

By using MyBatis Spring Boot Starter, most of the traditional XML configuration is eliminated, as mapper scanning and core settings are handled automatically by Spring Boot.

Service Layer

The service layer implementation class (BookServiceImpl) will be injected by the BookRepository mapper.


@RequiredArgsConstructor
@Service
public class BookServiceImpl implements BookService {

private final BookRepository repository;

@Override
public Book create(Book book) {
if (repository.findByTitle(book.getTitle()) != null) {
throw new DuplicateException(
MessageFormat.format(
"Book {0} already exists in the system",
book.getTitle()
)
);
}

repository.insert(book);
return repository.findByTitle(book.getTitle());
}

@Override
public List<Book> getAll() {
return repository.findAll();
}

@Override
public Book getOne(long id) {
Book book = repository.findById(id);
if (book == null) {
throw new DataNotFoundException(
MessageFormat.format("Book id {0} not found", id)
);
}
return book;
}

@Override
public void deleteById(long id) {
if (repository.findById(id) == null) {
throw new DataNotFoundException(
MessageFormat.format("Book id {0} not found", id)
);
}

repository.deleteById(id);
}

@Override
public Book getByTitle(String title) {
return repository.findByTitle(title);
}
}

The service layer encapsulates business logic and keeps controllers thin.

Create Book Controller

Create the REST controller to expose the API endpoints:


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

private final BookService bookService;

@PostMapping()
public ResponseEntity<SuccessResponse> create(@RequestBody @Valid Book book) {
if (!ObjectUtils.isEmpty(book.getId())) {
throw new BadRequestException("A new data cannot already have an ID");
}

return new ResponseEntity<>(
new SuccessResponse(bookService.create(book), "Successful registration"),
HttpStatus.CREATED);
}

@GetMapping
public ResponseEntity<SuccessResponse> getAll() {
List<Book> books = bookService.getAll();

return new ResponseEntity<>(new SuccessResponse(books, MessageFormat.format("{0} Results found", books.size())), HttpStatus.OK);
}

@GetMapping("/{id}")
public ResponseEntity<SuccessResponse> getOne(@PathVariable Long id) {
Book book = bookService.getOne(id);
return new ResponseEntity<>(
new SuccessResponse(book, "Result found"), HttpStatus.OK);
}

@DeleteMapping("/{id}")
public ResponseEntity<SuccessResponse> delete(@PathVariable Long id) {
bookService.deleteById(id);
return new ResponseEntity<>(
new SuccessResponse(null, "Deletion completed successfully"), HttpStatus.OK);
}

}

Test the application

Now we can run our application and test it.

  • Create Book
  • Get All Book
  • Get By book Id
  • Delete By book Id
DELETE http://localhost:8080/api/books/101

Conclusion

🏁 Well done !!. In this story, we’ve built a complete REST API using Spring Boot 4, MyBatis, and PostgreSQL with proper database migration management using Liquibase. This architecture gives you the flexibility of writing custom SQL while benefiting from Spring Boot’s powerful features.

MyBatis shines when you need fine-grained control over your SQL queries, especially for complex joins, custom result mappings, or performance-critical operations. Combined with Spring Boot’s convention-over-configuration approach and Liquibase’s robust database versioning, you have a solid foundation for building production-ready applications.

The complete source code is available on GitHub.

Support me through GitHub Sponsors.

Thank you for reading!! See you in the next story.

Last updated: February 1, 2026

References

👉 Link to Medium blog

Related Posts