CODE WITH SIBIN

Solving Real Problems with Real Code


Building REST CRUD APIs with Spring Boot, MyBatis (XML Mapping), and PostgreSQL

This comprehensive guide will walk you through creating a complete RESTful API with Spring Boot, MyBatis with XML mapping, and PostgreSQL. We'll cover everything from project setup to advanced topics like Postman testing and unit testing.

Table of Contents

  1. Project Setup
  2. Database Configuration
  3. Entity Class
  4. MyBatis Mapper (XML)
  5. Repository Layer
  6. Service Layer
  7. Controller Layer
  8. Exception Handling
  9. Postman Testing
  10. Unit Testing
  11. Integration Testing

Project Setup

First, let's create a new Spring Boot project using Spring Initializr or your IDE.

Dependencies (pom.xml)

<?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.1.0</version>
        <relativePath/>
    </parent>
    
    <groupId>com.example</groupId>
    <artifactId>mybatis-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>mybatis-demo</name>
    <description>Demo project for Spring Boot with MyBatis</description>
    
    <properties>
        <java.version>17</java.version>
        <mybatis-spring-boot.version>3.0.1</mybatis-spring-boot.version>
        <postgresql.version>42.5.4</postgresql.version>
    </properties>
    
    <dependencies>
        <!-- Spring Boot Starters -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- MyBatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>${mybatis-spring-boot.version}</version>
        </dependency>
        
        <!-- PostgreSQL -->
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>${postgresql.version}</version>
        </dependency>
        
        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        
        <!-- Testing -->
        <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>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Database Configuration

application.properties

# Server port
server.port=8080

# Database configuration
spring.datasource.url=jdbc:postgresql://localhost:5432/mybatis_demo
spring.datasource.username=postgres
spring.datasource.password=yourpassword
spring.datasource.driver-class-name=org.postgresql.Driver

# MyBatis configuration
mybatis.mapper-locations=classpath:mappers/*.xml
mybatis.type-aliases-package=com.example.mybatisdemo.model

# HikariCP configuration (optional)
spring.datasource.hikari.connection-timeout=20000
spring.datasource.hikari.maximum-pool-size=5

Create Database Table

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(100) NOT NULL UNIQUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Entity Class

package com.example.mybatisdemo.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private Long id;
    private String username;
    private String email;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

MyBatis Mapper (XML)

Create src/main/resources/mappers/UserMapper.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mybatisdemo.mapper.UserMapper">

    <resultMap id="userResultMap" type="User">
        <id property="id" column="id"/>
        <result property="username" column="username"/>
        <result property="email" column="email"/>
        <result property="createdAt" column="created_at"/>
        <result property="updatedAt" column="updated_at"/>
    </resultMap>

    <select id="findAll" resultMap="userResultMap">
        SELECT * FROM users
    </select>

    <select id="findById" parameterType="Long" resultMap="userResultMap">
        SELECT * FROM users WHERE id = #{id}
    </select>

    <insert id="insert" parameterType="User" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO users (username, email, created_at, updated_at)
        VALUES (#{username}, #{email}, #{createdAt}, #{updatedAt})
    </insert>

    <update id="update" parameterType="User">
        UPDATE users
        SET username = #{username},
            email = #{email},
            updated_at = #{updatedAt}
        WHERE id = #{id}
    </update>

    <delete id="deleteById" parameterType="Long">
        DELETE FROM users WHERE id = #{id}
    </delete>
</mapper>

Repository Layer

Mapper Interface

package com.example.mybatisdemo.mapper;

import com.example.mybatisdemo.model.User;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface UserMapper {
    List<User> findAll();
    User findById(Long id);
    void insert(User user);
    void update(User user);
    void deleteById(Long id);
}

Service Layer

Service Interface

package com.example.mybatisdemo.service;

import com.example.mybatisdemo.model.User;

import java.util.List;

public interface UserService {
    List<User> getAllUsers();
    User getUserById(Long id);
    User createUser(User user);
    User updateUser(Long id, User user);
    void deleteUser(Long id);
}

Service Implementation

package com.example.mybatisdemo.service.impl;

import com.example.mybatisdemo.exception.ResourceNotFoundException;
import com.example.mybatisdemo.mapper.UserMapper;
import com.example.mybatisdemo.model.User;
import com.example.mybatisdemo.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.List;

@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {

    private final UserMapper userMapper;

    @Override
    public List<User> getAllUsers() {
        return userMapper.findAll();
    }

    @Override
    public User getUserById(Long id) {
        return userMapper.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + id));
    }

    @Override
    public User createUser(User user) {
        LocalDateTime now = LocalDateTime.now();
        user.setCreatedAt(now);
        user.setUpdatedAt(now);
        userMapper.insert(user);
        return user;
    }

    @Override
    public User updateUser(Long id, User userDetails) {
        User user = getUserById(id);
        
        user.setUsername(userDetails.getUsername());
        user.setEmail(userDetails.getEmail());
        user.setUpdatedAt(LocalDateTime.now());
        
        userMapper.update(user);
        return user;
    }

    @Override
    public void deleteUser(Long id) {
        User user = getUserById(id);
        userMapper.deleteById(user.getId());
    }
}

Controller Layer

package com.example.mybatisdemo.controller;

import com.example.mybatisdemo.model.User;
import com.example.mybatisdemo.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @GetMapping
    public ResponseEntity<List<User>> getAllUsers() {
        List<User> users = userService.getAllUsers();
        return new ResponseEntity<>(users, HttpStatus.OK);
    }

    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        User user = userService.getUserById(id);
        return new ResponseEntity<>(user, HttpStatus.OK);
    }

    @PostMapping
    public ResponseEntity<User> createUser(@RequestBody User user) {
        User createdUser = userService.createUser(user);
        return new ResponseEntity<>(createdUser, HttpStatus.CREATED);
    }

    @PutMapping("/{id}")
    public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User user) {
        User updatedUser = userService.updateUser(id, user);
        return new ResponseEntity<>(updatedUser, HttpStatus.OK);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.deleteUser(id);
        return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }
}

Exception Handling

Custom Exception

package com.example.mybatisdemo.exception;

public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}

Global Exception Handler

package com.example.mybatisdemo.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.Map;

@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<Object> handleResourceNotFoundException(
            ResourceNotFoundException ex, WebRequest request) {
        
        Map<String, Object> body = new LinkedHashMap<>();
        body.put("timestamp", LocalDateTime.now());
        body.put("message", ex.getMessage());
        
        return new ResponseEntity<>(body, HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<Object> handleGlobalException(
            Exception ex, WebRequest request) {
        
        Map<String, Object> body = new LinkedHashMap<>();
        body.put("timestamp", LocalDateTime.now());
        body.put("message", "An error occurred");
        
        return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

Postman Testing

Here are the Postman requests you can use to test your API:

1. Create User (POST)

  • URL: http://localhost:8080/api/v1/users
  • Method: POST
  • Body (raw JSON):
{
    "username": "john_doe",
    "email": "john@example.com"
}

2. Get All Users (GET)

  • URL: http://localhost:8080/api/v1/users
  • Method: GET

3. Get User by ID (GET)

  • URL: http://localhost:8080/api/v1/users/1
  • Method: GET

4. Update User (PUT)

  • URL: http://localhost:8080/api/v1/users/1
  • Method: PUT
  • Body (raw JSON):
{
    "username": "john_doe_updated",
    "email": "john.updated@example.com"
}

5. Delete User (DELETE)

  • URL: http://localhost:8080/api/v1/users/1
  • Method: DELETE

Unit Testing

Service Layer Test

package com.example.mybatisdemo.service;

import com.example.mybatisdemo.exception.ResourceNotFoundException;
import com.example.mybatisdemo.mapper.UserMapper;
import com.example.mybatisdemo.model.User;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class UserServiceImplTest {

    @Mock
    private UserMapper userMapper;

    @InjectMocks
    private UserServiceImpl userService;

    private User user1;
    private User user2;

    @BeforeEach
    void setUp() {
        user1 = new User(1L, "user1", "user1@example.com", 
                        LocalDateTime.now(), LocalDateTime.now());
        user2 = new User(2L, "user2", "user2@example.com", 
                        LocalDateTime.now(), LocalDateTime.now());
    }

    @Test
    void getAllUsers_ShouldReturnAllUsers() {
        // Arrange
        List<User> expectedUsers = Arrays.asList(user1, user2);
        when(userMapper.findAll()).thenReturn(expectedUsers);

        // Act
        List<User> actualUsers = userService.getAllUsers();

        // Assert
        assertEquals(2, actualUsers.size());
        assertEquals(expectedUsers, actualUsers);
        verify(userMapper, times(1)).findAll();
    }

    @Test
    void getUserById_WithValidId_ShouldReturnUser() {
        // Arrange
        when(userMapper.findById(1L)).thenReturn(Optional.of(user1));

        // Act
        User actualUser = userService.getUserById(1L);

        // Assert
        assertNotNull(actualUser);
        assertEquals(user1.getId(), actualUser.getId());
        verify(userMapper, times(1)).findById(1L);
    }

    @Test
    void getUserById_WithInvalidId_ShouldThrowException() {
        // Arrange
        when(userMapper.findById(99L)).thenReturn(Optional.empty());

        // Act & Assert
        assertThrows(ResourceNotFoundException.class, () -> {
            userService.getUserById(99L);
        });
        verify(userMapper, times(1)).findById(99L);
    }

    @Test
    void createUser_ShouldSaveAndReturnUser() {
        // Arrange
        User newUser = new User(null, "newUser", "new@example.com", null, null);
        when(userMapper.insert(any(User.class))).thenAnswer(invocation -> {
            User user = invocation.getArgument(0);
            user.setId(3L);
            return null;
        });

        // Act
        User createdUser = userService.createUser(newUser);

        // Assert
        assertNotNull(createdUser.getId());
        assertEquals("newUser", createdUser.getUsername());
        assertNotNull(createdUser.getCreatedAt());
        verify(userMapper, times(1)).insert(any(User.class));
    }

    @Test
    void updateUser_WithValidId_ShouldUpdateUser() {
        // Arrange
        User existingUser = new User(1L, "oldUser", "old@example.com", 
                                    LocalDateTime.now(), LocalDateTime.now());
        User updatedDetails = new User(null, "newUser", "new@example.com", null, null);

        when(userMapper.findById(1L)).thenReturn(Optional.of(existingUser));
        doNothing().when(userMapper).update(any(User.class));

        // Act
        User updatedUser = userService.updateUser(1L, updatedDetails);

        // Assert
        assertEquals("newUser", updatedUser.getUsername());
        assertEquals("new@example.com", updatedUser.getEmail());
        assertNotNull(updatedUser.getUpdatedAt());
        verify(userMapper, times(1)).findById(1L);
        verify(userMapper, times(1)).update(any(User.class));
    }

    @Test
    void deleteUser_WithValidId_ShouldDeleteUser() {
        // Arrange
        when(userMapper.findById(1L)).thenReturn(Optional.of(user1));
        doNothing().when(userMapper).deleteById(1L);

        // Act
        userService.deleteUser(1L);

        // Assert
        verify(userMapper, times(1)).findById(1L);
        verify(userMapper, times(1)).deleteById(1L);
    }
}

Controller Layer Test

package com.example.mybatisdemo.controller;

import com.example.mybatisdemo.model.User;
import com.example.mybatisdemo.service.UserService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class UserControllerTest {

    @Mock
    private UserService userService;

    @InjectMocks
    private UserController userController;

    private User user1;
    private User user2;

    @BeforeEach
    void setUp() {
        user1 = new User(1L, "user1", "user1@example.com", 
                        LocalDateTime.now(), LocalDateTime.now());
        user2 = new User(2L, "user2", "user2@example.com", 
                        LocalDateTime.now(), LocalDateTime.now());
    }

    @Test
    void getAllUsers_ShouldReturnAllUsers() {
        // Arrange
        List<User> expectedUsers = Arrays.asList(user1, user2);
        when(userService.getAllUsers()).thenReturn(expectedUsers);

        // Act
        ResponseEntity<List<User>> response = userController.getAllUsers();

        // Assert
        assertEquals(HttpStatus.OK, response.getStatusCode());
        assertEquals(2, response.getBody().size());
        verify(userService, times(1)).getAllUsers();
    }

    @Test
    void getUserById_WithValidId_ShouldReturnUser() {
        // Arrange
        when(userService.getUserById(1L)).thenReturn(user1);

        // Act
        ResponseEntity<User> response = userController.getUserById(1L);

        // Assert
        assertEquals(HttpStatus.OK, response.getStatusCode());
        assertEquals(user1, response.getBody());
        verify(userService, times(1)).getUserById(1L);
    }

    @Test
    void createUser_ShouldReturnCreatedUser() {
        // Arrange
        User newUser = new User(null, "newUser", "new@example.com", null, null);
        User savedUser = new User(3L, "newUser", "new@example.com", 
                                LocalDateTime.now(), LocalDateTime.now());
        
        when(userService.createUser(newUser)).thenReturn(savedUser);

        // Act
        ResponseEntity<User> response = userController.createUser(newUser);

        // Assert
        assertEquals(HttpStatus.CREATED, response.getStatusCode());
        assertEquals(savedUser, response.getBody());
        verify(userService, times(1)).createUser(newUser);
    }

    @Test
    void updateUser_WithValidId_ShouldReturnUpdatedUser() {
        // Arrange
        User updatedDetails = new User(null, "updatedUser", "updated@example.com", null, null);
        User updatedUser = new User(1L, "updatedUser", "updated@example.com", 
                                  user1.getCreatedAt(), LocalDateTime.now());
        
        when(userService.updateUser(1L, updatedDetails)).thenReturn(updatedUser);

        // Act
        ResponseEntity<User> response = userController.updateUser(1L, updatedDetails);

        // Assert
        assertEquals(HttpStatus.OK, response.getStatusCode());
        assertEquals(updatedUser, response.getBody());
        verify(userService, times(1)).updateUser(1L, updatedDetails);
    }

    @Test
    void deleteUser_WithValidId_ShouldReturnNoContent() {
        // Arrange
        doNothing().when(userService).deleteUser(1L);

        // Act
        ResponseEntity<Void> response = userController.deleteUser(1L);

        // Assert
        assertEquals(HttpStatus.NO_CONTENT, response.getStatusCode());
        verify(userService, times(1)).deleteUser(1L);
    }
}

Integration Testing

Create an integration test to test the entire flow from controller to database:

package com.example.mybatisdemo;

import com.example.mybatisdemo.model.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import com.fasterxml.jackson.databind.ObjectMapper;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringBootTest
@AutoConfigureMockMvc
@Testcontainers
class UserControllerIntegrationTest {

    @Container
    public static PostgreSQLContainer<?> postgreSQLContainer = new PostgreSQLContainer<>("postgres:13")
            .withDatabaseName("testdb")
            .withUsername("test")
            .withPassword("test");

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    void testCreateAndGetUser() throws Exception {
        // Create a new user
        User newUser = new User();
        newUser.setUsername("testuser");
        newUser.setEmail("test@example.com");

        MvcResult createResult = mockMvc.perform(post("/api/v1/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(newUser)))
                .andExpect(status().isCreated())
                .andReturn();

        User createdUser = objectMapper.readValue(
                createResult.getResponse().getContentAsString(), User.class);

        // Get the created user
        mockMvc.perform(get("/api/v1/users/" + createdUser.getId()))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(createdUser.getId()))
                .andExpect(jsonPath("$.username").value("testuser"))
                .andExpect(jsonPath("$.email").value("test@example.com"));
    }

    @Test
    void testUpdateUser() throws Exception {
        // First create a user
        User newUser = new User();
        newUser.setUsername("original");
        newUser.setEmail("original@example.com");

        MvcResult createResult = mockMvc.perform(post("/api/v1/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(newUser)))
                .andExpect(status().isCreated())
                .andReturn();

        User createdUser = objectMapper.readValue(
                createResult.getResponse().getContentAsString(), User.class);

        // Update the user
        User updatedUser = new User();
        updatedUser.setUsername("updated");
        updatedUser.setEmail("updated@example.com");

        mockMvc.perform(put("/api/v1/users/" + createdUser.getId())
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(updatedUser)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.username").value("updated"))
                .andExpect(jsonPath("$.email").value("updated@example.com"));
    }

    @Test
    void testDeleteUser() throws Exception {
        // First create a user
        User newUser = new User();
        newUser.setUsername("todelete");
        newUser.setEmail("delete@example.com");

        MvcResult createResult = mockMvc.perform(post("/api/v1/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(newUser)))
                .andExpect(status().isCreated())
                .andReturn();

        User createdUser = objectMapper.readValue(
                createResult.getResponse().getContentAsString(), User.class);

        // Delete the user
        mockMvc.perform(delete("/api/v1/users/" + createdUser.getId()))
                .andExpect(status().isNoContent());

        // Verify the user is deleted
        mockMvc.perform(get("/api/v1/users/" + createdUser.getId()))
                .andExpect(status().isNotFound());
    }
}

Additional Configuration

MyBatis Configuration Class (Optional)

package com.example.mybatisdemo.config;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan("com.example.mybatisdemo.mapper")
public class MyBatisConfig {
    // Additional MyBatis configuration can go here
}

Application Main Class

package com.example.mybatisdemo;

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

@SpringBootApplication
public class MybatisDemoApplication {

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

Conclusion

This comprehensive guide has covered:

  1. Setting up a Spring Boot project with MyBatis and PostgreSQL
  2. Configuring MyBatis with XML mappings
  3. Implementing CRUD operations
  4. Proper layering (Controller-Service-Repository)
  5. Exception handling
  6. Postman testing
  7. Unit testing with Mockito
  8. Integration testing with Testcontainers

The application follows RESTful principles and includes proper error handling and validation. You can extend this foundation by adding:

  • Pagination for GET all endpoints
  • DTOs for request/response separation
  • Validation annotations
  • Authentication and authorization
  • Swagger/OpenAPI documentation
  • More complex business logic as needed

Leave a Reply

Your email address will not be published. Required fields are marked *