Supercharging Productivity in Microservice Development With AI Tools

Building APIs and their related components is often a tedious task, especially when dealing with complex data models and architectures like microservices. Repetitive coding, setting up the configurations, and the overhead of building unit tests can quickly become time-consuming. Here's how AI tools, like ChatGPT, can be a game-changer. 

Harnessing AI Tools (Chat GPT) In API Development

To understand the capabilities of ChatGPT, let's dive into a hands-on example. We're tasked with developing a REST API to manage users' Personally Identifiable Information (PII). The process entails:

  1. Database object creation (Stored procedure)
  2. Repository (Interface for Database Interaction and Data Access Object)
  3. Service (Business Logic Layer)
  4. REST Controller (Exposing Endpoints)

Database Objects

To build a stored procedure, first provide the table structure to ChatGPT. After that, supply a sample stored procedure. This will allow ChatGPT to anchor to the stored procedure pattern and construct a new procedure for the specified table. Below is the table we provided:

MySQL
 
CREATE TABLE `user_pii_data_repo` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `user_id` int DEFAULT NULL,
  `loan_id` int DEFAULT NULL,
  `ext_partner_name` varchar(30)  NOT NULL,
  `ext_partner_source` varchar(30)  NOT NULL,
  `phase` varchar(20)  NOT NULL,
  `session_uuid` varchar(36)  DEFAULT NULL,
  `first_name` varchar(300)  DEFAULT NULL,
  `last_name` varchar(300)  DEFAULT NULL,
  `address1` varchar(300)  DEFAULT NULL,
  `address2` varchar(300)  DEFAULT NULL,
  `city` varchar(300)  DEFAULT NULL,
  `state` char(2)  DEFAULT NULL,
  `zip` varchar(300)  DEFAULT NULL,
  `ssn` varchar(300)  DEFAULT NULL,
  `dob` date DEFAULT NULL,
  `phone` varchar(300)  DEFAULT NULL,
  `payload_path` varchar(500)  DEFAULT NULL,
  PRIMARY KEY (`id`)
);


Here is the stored procedure that has been generated:

MySQL
 
CREATE PROCEDURE `p_get_user_pii_data`(IN v_user_id INT, IN v_loan_id INT)
BEGIN
    SELECT 
        id, user_id, loan_id, ext_partner_name,
        ext_partner_source, phase, session_uuid, first_name,
        last_name, address1, address2, city,
        state, zip, ssn, dob,
        phone, payload_path
    FROM 
        user_pii_data_repo
    WHERE
        user_id = v_user_id
    AND
        loan_id = v_loan_id;
END;


Here is the conversation link.

Repository (Interface for Database Interaction and Data Access Object)

In our endeavor to construct the repository layer, we employed a dedicated ChatGPT thread. We supplied it with the previously mentioned stored procedure and table definition. Leveraging ChatGPT's capabilities, we successfully generated:

  1. A JPA entity.
  2. A comprehensive Repository class encompassing both the interface and its implementation.

Post-generation, we tasked ChatGPT with the creation of unit test cases. Impressively, the outcomes were near-perfect. A few minor adjustments were necessary, predominantly related to namespaces and some other minor details.

Below are the generated classes and code: 

Repository Interface and Implementation

Java
 
package com.demo.app.api.core.repository.user_pii;
import com.demo.app.api.core.model.user_pii.UserPiiData;
import java.util.List;
public interface UserPiiDataRepository {
    List<UserPiiData> getUserPiiData(Integer userId, Integer loanId);
}
Java
 
package com.demo.app.api.core.repository.user_pii;
import com.demo.app.api.core.model.user_pii.UserPiiData;
import com.demo.app.api.core.repository.EntityManagerThreadLocalHelper;
import com.demo.app.api.core.services.common.CryptoService;
import com.demo.app.api.utils.Utility;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Repository;
import javax.persistence.*;
import java.util.List;

@Repository
@Qualifier("userPiiDataRepository")
public class UserPiiDataRepositoryImpl implements UserPiiDataRepository {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    EntityManagerThreadLocalHelper threadLocalHelper;

    @Autowired
    CryptoService cryptoService;

    @Override
    public List<UserPiiData> getUserPiiData(Integer userId, Integer loanId) {
        List<UserPiiData> results = null;
        try {
            EntityManager entityManager = threadLocalHelper.getEntityManager();
            StoredProcedureQuery query = entityManager.createStoredProcedureQuery(UserPiiDataDbConstants.PROC_UW_AP_GET_USER_PII_DATA, UserPiiData.class);

            query.registerStoredProcedureParameter(UserPiiDataDbConstants.V_PARAM_USER_ID, Integer.class, ParameterMode.IN);
            query.registerStoredProcedureParameter(UserPiiDataDbConstants.V_PARAM_LOAN_ID, Integer.class, ParameterMode.IN);

            query.setParameter(UserPiiDataDbConstants.V_PARAM_USER_ID, userId);
            query.setParameter(UserPiiDataDbConstants.V_PARAM_LOAN_ID, loanId);

            query.execute();
            results = query.getResultList();
            for (UserPiiData userPiiData :
                    results) {
                if (userPiiData != null) {
                    if (userPiiData.getFirstName() != null) {
                        String decryptedFirstName = this.cryptoService.decrypt(userPiiData.getFirstName());
                        userPiiData.setFirstName(decryptedFirstName);
                    }

                    .....

                    if (userPiiData.getDob() != null) {
                        String formattedDate = Utility.getFormattedDobMMDDYYYY(userPiiData.getDob());
                        userPiiData.setFormattedDob(formattedDate);
                    }
                }
                entityManager.detach(userPiiData);
            }
            threadLocalHelper.closeEntityManager();
        } catch(Exception ex) {
            logger.error("UserPiiDataRepositoryImpl::getUserPiiData - an error occurred while fetching user pii data", ex);
            threadLocalHelper.closeEntityManager();
        }
        return results;
    }
}


Java
 
package com.demo.app.api.core.repository.user_pii;

public interface UserPiiDataDbConstants {
    String PROC_UW_AP_GET_USER_PII_DATA = "p_uw_ap_get_user_pii_data";
    String V_PARAM_USER_ID = "v_user_id";
    String V_PARAM_LOAN_ID = "v_loan_id";
}


Unit Tests

Java
 

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

import com.demo.app.api.core.model.user_pii.UserPiiData;
import com.demo.app.api.core.repository.EntityManagerReadOnlyThreadLocalHelper;
import com.demo.app.api.core.services.common.CryptoService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import javax.persistence.EntityManager;
import javax.persistence.StoredProcedureQuery;
import java.util.Collections;
import java.util.Date;
import java.util.List;

public class UserPiiDataRepositoryImplTest {

    @InjectMocks
    private UserPiiDataRepositoryImpl userPiiDataRepository;

    @Mock
    private EntityManagerReadOnlyThreadLocalHelper threadLocalHelper;

    @Mock
    private CryptoService cryptoService;

    @Mock
    private EntityManager entityManager;

    @Mock
    private StoredProcedureQuery storedProcedureQuery;

    @BeforeEach
    public void setUp() {
        MockitoAnnotations.openMocks(this);
    }

    @Test
    public void testGetUserPiiData() {
        // Arrange
        UserPiiData sampleData = new UserPiiData();
        sampleData.setFirstName("encryptedFirstName");
        sampleData.setLastName("encryptedLastName");
        sampleData.setAddress1("encryptedAddress1");
        sampleData.setCity("encryptedCity");
        sampleData.setZip("encryptedZip");
        sampleData.setSsn("encryptedSsn");
        sampleData.setPhone("encryptedPhone");
        sampleData.setDob(new Date());

        when(threadLocalHelper.getEntityManager()).thenReturn(entityManager);
        when(entityManager.createStoredProcedureQuery(UserPiiDataDbConstants.PROC_UW_AP_GET_USER_PII_DATA, UserPiiData.class)).thenReturn(storedProcedureQuery);
        when(storedProcedureQuery.getResultList()).thenReturn(Collections.singletonList(sampleData));

        when(cryptoService.decrypt("encryptedFirstName")).thenReturn("John");
        when(cryptoService.decrypt("encryptedLastName")).thenReturn("Doe");
        when(cryptoService.decrypt("encryptedAddress1")).thenReturn("123 Main St.");
        when(cryptoService.decrypt("encryptedCity")).thenReturn("Test City");
        when(cryptoService.decrypt("encryptedZip")).thenReturn("12345");
        when(cryptoService.decrypt("encryptedSsn")).thenReturn("123-45-6789");
        when(cryptoService.decrypt("encryptedPhone")).thenReturn("123-456-7890");

         // Act
        List<UserPiiData> result = userPiiDataRepository.getUserPiiData(1, 100);

        // Assert
        assertEquals(1, result.size());
        UserPiiData fetchedData = result.get(0);

        assertEquals("John", fetchedData.getFirstName());
        assertEquals("Doe", fetchedData.getLastName());
        assertEquals("123 Main St.", fetchedData.getAddress1());
        assertEquals("Test City", fetchedData.getCity());
        assertEquals("12345", fetchedData.getZip());
        assertEquals("123-45-6789", fetchedData.getSsn());
        assertEquals("123-456-7890", fetchedData.getPhone());
    }
}


During our initial execution, we encountered challenges with the code generated for the repository implementation. The data stored in our database was encrypted, necessitating decryption of the persisted values. We refined the repository layer's code to address this, integrating decryption mechanisms.

After implementing the modifications, we supplied ChatGPT with the updated repository code, seeking a corresponding unit test case. Remarkably, ChatGPT furnished us with a perfectly functional unit test case.

For those interested in the detailed dialogue, here's the link to our ChatGPT conversation thread.

Building Business Service layer

To deepen our exploration, we extended our requirements to an existing ChatGPT service sample, specifying dependencies like the JPA entity and the repository interface.

The AI tool promptly generated classes. The Service Interface outlined a clear contract for fetching user PII data based on user and loan IDs. Its implementation, UserPIIDataServiceImpl, utilized the JPA repository to fetch the required data, and if null, would return an empty list — a thoughtful touch for avoiding potential null pointer exceptions.

The unit test, UserPIIDataServiceImplTest, was comprehensive. Using Mockito for mock objects, the test verified the service's functionality by arranging mock data, calling the service, and asserting the expected results.

Below are the generated classes and code:

Service Interface and Implementation

Java
 
package com.demo.app.api.core.services.user_pii;
import com.demo.app.api.core.model.user_pii.UserPiiData;
import java.util.List;
public interface UserPIIDataService {
    List<UserPiiData> getUserPiiData(Integer userId, Integer loanId);
}
Java
 
package com.demo.app.api.core.services.user_pii;

import com.demo.app.api.core.model.user_pii.UserPiiData;
import com.demo.app.api.core.repository.user_pii.UserPiiDataRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Collections;
import java.util.List;

@Service
public class UserPIIDataServiceImpl implements UserPIIDataService {

    @Autowired
    UserPiiDataRepository userPiiDataRepository;

    @Override
    public List<UserPiiData> getUserPiiData(Integer userId, Integer loanId) {
        List<UserPiiData> piiData = userPiiDataRepository.getUserPiiData(userId, loanId);
        return piiData != null ? piiData : Collections.emptyList();
    }
}


Unit Tests

Java
 
package com.demo.app.api.core.services.user_pii;

import com.demo.app.api.core.model.user_pii.UserPiiData;
import com.demo.app.api.core.repository.user_pii.UserPiiDataRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.Arrays;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;

public class UserPIIDataServiceImplTest {

    @Mock
    private UserPiiDataRepository userPiiDataRepository;

    @InjectMocks
    private UserPIIDataServiceImpl userPIIDataService;

    @BeforeEach
    public void setup() {
        MockitoAnnotations.openMocks(this);
    }

    @Test
    public void testGetUserPiiData() {
        // Arrange
        Integer userId = 1;
        Integer loanId = 101;
        UserPiiData data1 = new UserPiiData();
        data1.setUserId(userId);
        data1.setLoanId(loanId);
        data1.setFirstName("John");
        UserPiiData data2 = new UserPiiData();
        data2.setUserId(userId);
        data2.setLoanId(loanId);
        data2.setFirstName("Jane");
        List<UserPiiData> mockResponse = Arrays.asList(data1, data2);
        when(userPiiDataRepository.getUserPiiData(userId, loanId)).thenReturn(mockResponse);

        // Act
        List<UserPiiData> result = userPIIDataService.getUserPiiData(userId, loanId);

        // Assert
        assertEquals(2, result.size());
        assertEquals("John", result.get(0).getFirstName());
        assertEquals("Jane", result.get(1).getFirstName());
    }
}


Here is the chat thread for building the service layer.

Final Step: REST Controller Generation

Having set up our foundational layers, it was time to build our REST Controller. For this, we leveraged a previously existing REST Controller sample as a base template. We provided ChatGPT with this sample alongside the service interface and the necessary entity object.

In response, ChatGPT generated a RestController that was tailored to our requirements. Below are the generated classes and code:

Java
 
package com.demo.app.api.controller.user_pii;

import com.demo.app.api.core.model.user_pii.UserPiiData;
import com.demo.app.api.core.services.user_pii.UserPIIDataService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@CrossOrigin(origins = "*")
@RestController
@RequestMapping("/api/mis/v1/ap")
@Api(tags = "Controller for fetching User's PII data")
public class UserPIIDataController {

    @Autowired
    UserPIIDataService userPIIDataService;

    @GetMapping(path = "/pii-data/user/{userId}/loan/{loanId}", produces = "application/json")
    @ApiOperation("Fetch user's PII data by user ID and loan ID")
    @ApiResponses(value = {
            @ApiResponse(code = 200, message = "Successfully fetched user's PII data"),
            @ApiResponse(code = 400, message = "Bad request"),
            @ApiResponse(code = 404, message = "Not found"),
            @ApiResponse(code = 500, message = "Internal server error")
    })
    public List<UserPiiData> getUserPiiDataByUserIdAndLoanId(
            @PathVariable(name = "userId") Integer userId,
            @PathVariable(name = "loanId") Integer loanId) {
        return this.userPIIDataService.getUserPiiData(userId, loanId);
    }
}


A standout feature of the auto-generated code was the inclusion of annotations, specifically ApiResponse and ApiOperation. Without ChatGPT's intervention, these essential annotations might have been inadvertently overlooked, which can affect the clarity and documentation quality of the API.

Subsequent to the REST Controller generation, we sought ChatGPT's assistance in producing unit test cases. Initially, the generated tests were designed as if they were calling the actual API. However, our intention was to strictly test the controller's code. After providing the necessary instructions to narrow down the focus, ChatGPT effectively delivered the tailored unit test code. Here's a snapshot of the generated unit test:

Java
 
package com.demo.app.api.controller.user_pii;

import com.demo.app.api.core.model.user_pii.UserPiiData;
import com.demo.app.api.core.services.user_pii.UserPIIDataService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.Collections;
import java.util.List;

import static org.mockito.Mockito.when;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class UserPIIDataControllerTest {

    @Mock
    private UserPIIDataService userPIIDataService;

    @InjectMocks
    private UserPIIDataController userPIIDataController;

    @BeforeEach
    public void setup() {
        MockitoAnnotations.openMocks(this);
    }

    @Test
    public void testGetUserPiiDataByUserIdAndLoanId() {
        Integer userId = 1;
        Integer loanId = 1;
        UserPiiData userPiiData = new UserPiiData();
        userPiiData.setUserId(userId);
        userPiiData.setLoanId(loanId);
        userPiiData.setFirstName("John");
        userPiiData.setLastName("Doe");
        List<UserPiiData> expectedUserPiiDataList = Collections.singletonList(userPiiData);

        when(userPIIDataService.getUserPiiData(userId, loanId)).thenReturn(expectedUserPiiDataList);

        List<UserPiiData> actualUserPiiDataList = userPIIDataController.getUserPiiDataByUserIdAndLoanId(userId, loanId);

        assertEquals(expectedUserPiiDataList, actualUserPiiDataList);
    }
}


Finally, to ensure everything was functioning as anticipated, we conducted a verification of the API endpoint using Postman. Delightfully, the API behaved exactly as expected, showcasing the practical applicability and precision of the auto-generated code. Here is the chat thread.

The above example might seem straightforward, but we've also applied this approach to build API endpoints for inserting data observing a consistent pattern. The real advantage shines through when dealing with extensive tables, say with 30 columns. Manually defining stored procedure parameters and constructing entities with a multitude of attributes — each requiring accurate column mapping — can be a tedious and error-prone process. However, leveraging tools like ChatGPT or other AI utilities eliminates these repetitive tasks. As a result, developers can produce more efficient, well-documented code with reduced effort.

Conclusion

The technological realm is evolving rapidly. With the advent of AI tools like ChatGPT, developers now possess powerful allies in their coding endeavors. By automating the more tedious and repetitive tasks in API development, these tools not only streamline the process but also drastically improve code quality and accuracy. The experiences shared in this article are a testament to the fact that AI's potential to revolutionize software development isn't mere speculation — it's a reality we're beginning to embrace. As we forge ahead, such collaborations between humans and machines will undoubtedly redefine the landscape of software engineering, opening doors to new possibilities and efficiency levels previously thought unattainable.

 

 

 

 

Top