Skip to content

Build fintech application with Spring Boot, Vavr and Vue.js (Part 2 – Companies Backend)

Hello again! This post is the second part of my Spring Boot + Vue.js tutorial, however is actually the first one where we will do real coding. This time we will start from backend part and write REST API to deal with companies (vendors and customers) using Spring Boot and test-driven development paradigm. We will prepare all levels (POJOs, data layer, service layer, controller layer) and test everything.

Table of contents

What we do in this part

Before we will have our hands dirty with code, let quickly define goals for this part. So, this time we work on backend level. Take a look on objectives of this post:

  1. Create a complete REST API endpoint for companies (customers and vendors)
  2. Learn how to work with Spring Data Mongo repositories
  3. Solve a problem with Vavr collections’ serialization/deserialization in Jackson
  4. Review how to use test driven development to write Spring services
  5. Test REST API using Postman

Let start!

Step 1. Define entities

We use entities to define data models. In our accounting app we have companies. Each object represents a company (business), that can be either vendor or customer. As they are same, we can create one model Company and distinguish these types using field type. Also, each company has its address, that can repeat (in real world app) for shipping and billing address – so we need to have a nested entity Address. Take a look on the following code snippet:

@Value
@AllArgsConstructor
public class Address {
    String city;
    String postalCode;
    String addressLine;
}

This model describes address data model. Note these points:

  • @Value annotation comes from Lombok and is used to create immutable classes where all fields are marked as private and final. That means, that by adding this annotation we don’t need to explicitly specify modifiers for fields.
  • Our models are immutable – that means, their states can’t be changed after construction. The way to achieve immutability is to have final fields, no setters and define values in constructor. So we need…
  • @AllArgsConstructor annotation!

Next POJO is Company. This model is different, as it is also used as database model, so has specific Mongo’s annotations:

@Value
@AllArgsConstructor
@Document(collection="companies")
public class Company {
    @Id String companyId;
    String userId;
    String name;
    String type;
    String taxId;
    Address address;
}

Basically, we need to remember here following things:

  • @Id maps companyId with _id field of MongoDB document
  • @Document marks our model as MongoDB document and defines collection’s name we’ll use

However, this is not all what we need to do for entities. The issue with Jackson and Lombok and immutables is deserialization. To avoid problems we also need to create a file called lombok.config (you can place it inside root folder) and put this line of code there:

lombok.anyConstructor.addConstructorProperties = true

Step 2. Create repository

Spring Data provides us with repositories that are used to manage underlaying data sources (like adding, removing, updating or querying data). Basically it works as follow: we extend CrudRepository marker interface for concrete domain object and Spring generates code for working with required datasource implementation (in our case MongoDB). CrudRepository offers us sophisticated CRUD functionality for the entity class, so we don’t need to write it ourselves:

public interface ICompanyRepository 
        extends CrudRepository<Company, String> {}

Here we extended CrudRepository<Entity, Id> for Company and String data types. However, out of the box, Spring Data Mongo works with native Java collections for querying entities and Optionals for one entity. Due to the fact, we use Vavr, we need to explicitly create methods in repository, that will return Vavr lists and Option:

public interface ICompanyRepository extends CrudRepository<Company, String> {

    List<Company> findByUserIdAndType (String userId, String type);

    Option<Company> findByCompanyId (String companyId);
}

NB here we declare custom query methods, using method name. You can take a look to Spring Data docs to learn more about writing custom queries.

Step 3. Write service

Now we move to business logic! With repositories we don’t need to do a lot of work (thanks Spring!) as framework generates them for us. But services are components, that contains actual business logic of our apps, so here is the most important part of work. We will use test driven development approach here. This means that we:

  1. Define a service contract (interface)
  2. Provide an empty implementation
  3. Write a test
  4. Fail test
  5. Provide an implementation
  6. Pass test

Step 3.1. ICompanyService interface

In ICompanyService interface, we define methods, that the actual implementation of company service should have:

public interface ICompanyService {

    Company createCompany (Company company);

    void removeCompany (String id);

    void updateCompany (Company company);

    Option<Company> findCompanyById (String id);

    List<Company> findVendors (String userId);

    List<Company> findCustomers (String userId);
}

Next, create an empty implementation:

@Component("ICompanyService")
public class CompanyServiceImpl implements ICompanyService {

    @Autowired
    private ICompanyRepository repository;

    @Override
    public Company createCompany(Company company) {
        return null;
    }

    @Override
    public void removeCompany(String id) {
        // ..
    }

    @Override
    public void updateCompany(Company company) {
        //..
    }

    @Override
    public Option<Company> findCompanyById(String id) {
        return null;
    }

    @Override
    public List<Company> findVendors(String userId) {
        return null;
    }

    @Override
    public List<Company> findCustomers(String userId) {
        return null;
    }

}

Note following Spring annotations:

  • @Component marks this implementation as a component of type ICompanyService, so then Spring injects the implementation to callers
  • @Autowired is an opposite annotation: it is used to define a dependency that is used by this caller class

Step 3.2. Write unit test

Test driven approach tells us to write a unit test first. Here we use JUnit 5 testing library, so if you are not familiar with it, here is my post on how to start with JUnit 5. First thing first, configure mocks. NB: many tutorials are outdated and use @RunWith annotation that leads to confuses. JUnit 5 uses an extension model, so we use @ExtendWith:

@ExtendWith(MockitoExtension.class)
public class CompanyServiceImplTest {

    @Mock private ICompanyRepository repository;
    @InjectMocks CompanyServiceImpl service;

}

I’d like also to create a couple of helper methods, that generate some mock data, which we can use in test methods:

private Company createMockCompany(String id, String name, String type){
    Address address = new Address("Tallinn", "10130", "Kiriku 6 tn");
    return new Company(id, "user", name, type, "12345", address);
}

private List<Company> createListOfCompanies (String type){
    List<Company> results = List
        .of(createMockCompany("12345", "Codesity", type),
            createMockCompany("12345", "Codesity", type),
            createMockCompany("12345", "Codesity", type),
            createMockCompany("12345", "Codesity", type),
            createMockCompany("12345", "Codesity", type));
    return results;
}

Now, we can write some testing logic. Check the code snippet below:

@Test
public void createCompanyTest(){
    Company data = createMockCompany("123456", "Codesity", "vendor");
    when(repository.save(data)).thenReturn(data);

    Company result = service.createCompany(data);

    assertEquals(data.getCompanyId(), result.getCompanyId());
    assertEquals(data.getName(), result.getName());
    assertEquals(data.getType(), result.getType());
}

@Test
public void findCompanySuccessTest(){
    String id = "123456";
    Company data = createMockCompany(id, "Codesity", "vendor");
    when(repository.findByCompanyId(id)).thenReturn(Option.of(data));

    Option<Company> result = repository.findByCompanyId(id);
    result.onEmpty(() -> fail("Nothing found"));
    Company company = result.get();
    assertEquals(data.getName(), company.getName());
    assertEquals(id, company.getCompanyId());
}

@Test
public void findCompanyFailedTest(){
    String id = "12345";
    when(repository.findByCompanyId(id)).thenReturn(Option.none());
    Option<Company> result = repository.findByCompanyId(id);
    if (result.isDefined()) fail();
}

@Test
public void findVendorsTest(){
    List<Company> vendors = createListOfCompanies("vendor");
    when(repository.findByUserIdAndType("user", "vendor")).thenReturn(vendors);

    List<Company> results = service.findVendors("user");
    assertEquals(vendors, results);
}

@Test
public void findCustomersTest(){
    List<Company> customers = createListOfCompanies("customer");
    when(repository.findByUserIdAndType("user", "customer")).thenReturn(customers);

    List<Company> results = service.findCustomers("user");
    assertEquals(customers, results);
}

Run this test and like TDD approach tells us, it was failed. We need to provide an implementation for service first.

Step 3.3. Implement company service

Here we do an actual logic of CompanyServiceImpl. Check this code:

@Component("ICompanyService")
public class CompanyServiceImpl implements ICompanyService {

    @Autowired
    private ICompanyRepository repository;

    @Override
    public Company createCompany(Company company) {
        return repository.save(company);
    }

    @Override
    public void removeCompany(String id) {
        repository.deleteById(id);
    }

    @Override
    public Option<Company> findCompanyById(String id) {
        return repository.findByCompanyId(id);
    }

    @Override
    public List<Company> findVendors(String userId) {
        return repository.findByUserIdAndType(userId, "vendor");
    }

    @Override
    public List<Company> findCustomers(String userId) {
        return repository.findByUserIdAndType(userId, "customer");
    }

}

The only missed place is updateCompany() method. Technically, Spring Data repositories do updating with same save() method like on creation. The repository checks if an entity provides an ID and if yes it updates existing record; otherwise – it creates new one. However, I’d like to provide own validation. Take a look on this code snippet below:

@Override
public void updateCompany(Company company) {
    repository.findByCompanyId(company.getCompanyId())
        .peek(data -> repository.save(company));
}

Here we use Vavr Option ‘s method to perform save() method only if record actually exists in database. It saves us from possible changes of repository’s implementation. Finally, run test again:

Image 1. CompanyServiceImplTest passed

Step 4. Create rest controller

The last element to create in this part is REST controller. This component is used to create an actual REST API and works with HTTP requests. First, create a class CompanyRestController.java

@RestController
@RequestMapping("/v1/companies")
@CrossOrigin(origins="*")
public class CompanyRestController {

    @Autowired
    private ICompanyService service;

}

What is important here?

  • @RequestMapping annotation which is applied to the whole controller defines a path URL for all handlers. So, all handlers with start with /v1/companies.
  • We inject a dependency of type ICompanyService. Spring resolves it (using @Component) and injects CompanyServiceImpl.
  • @CrossOrigin annotation deals with CORS. We have to configure this explicitly, as our client and backend live on different servers. Don’t panic – we will talk more about it in the next part. For now – this annotation is more than enough

Let now implement actual handler methods. Take a look on this code:

@PostMapping("/")
public ResponseEntity<Company> createCompany (@RequestBody Company payload){
    Company result = service.createCompany(payload);
    return ResponseEntity.ok(result);
}

@PutMapping("/")
public void updateCompany(@RequestBody Company payload){
    service.updateCompany(payload);
}

@DeleteMapping("/{companyId}")
public void removeCompany(@PathVariable String companyId){
    service.removeCompany(companyId);
}

@GetMapping("/one/{companyId}")
public ResponseEntity<Company> findOneCompany (@PathVariable String companyId){
    Option<Company> result = service.findCompanyById(companyId);
    if (result.isDefined()){
        Company company = result.get();
        return ResponseEntity.ok(company);
    }
    return ResponseEntity.notFound().build();
}

@GetMapping("/vendors/{userId}")
public ResponseEntity<List<Company>> findVendors (@PathVariable String userId){
    List<Company> results = service.findVendors(userId);
    return ResponseEntity.ok(results);
}

@GetMapping("/customers/{userId}")
public ResponseEntity<List<Company>> findCustomers (@PathVariable String userId){
    List<Company> results = service.findCustomers(userId);
    return ResponseEntity.ok(results);
}

Note following points:

  • To access body payload we use @RequestBody annotation to map a body object to our domain entity. Likewise we utilize @PathVariable for accessing params from URLs. NB that here I define path variables with same name like in mapping annotations. Otherwise, you need to specify a name explicitly as @PathVariable(name="").
  • We use ResponseEntity as return type. It is basically a complete HTTP response, that includes body and status code. So, we use notFound() method to create a 404 response.

Now we can test this API controller using Postman (or other HTTP client of your choice). Remember, that controller has address http://localhost:4567/v1/companies.

First, create new company:

Image 2. Company REST API – Create a new company

Then, assert that it was inserted to database:

Image 3. Company REST API – find one company by ID

And if we will remove it, we will no longer be able to access it:

Image 4. Companies REST API – after deletion

Let add some mock customers and try to retrieve them using http://localhost:4567/v1/companies/customers/{userId}:

Image 5. Companies REST API – Vavr List issue

Oops! Looks like Spring has a trouble with deserialization of Vavr lists. Let try to solve this issue.

Step 5. Solve Vavr deserialization problem

Deserialization technically means a process of convertation of domain objects to output stream (here to JSON string). The issue here is Jackson – a library that works with JSON in Spring – does not know how to deal with Vavr collections. And this is a big problem, because later when we need to serialize entities from JSON payload we will also have troubles. So, what can we do here? There are two ways.

Approach 1. Simple and stupid

The first idea here can be to output vanila Java lists, as Jackson knows how to deal with them:

@GetMapping("/vendors/{userId}")
public ResponseEntity<java.util.List<Company>> findVendors 
                    (@PathVariable String userId){
    List<Company> results = service.findVendors(userId);
    return ResponseEntity.ok(results.asJava());
}

@GetMapping("/customers/{userId}")
public ResponseEntity<java.util.List<Company>> findCustomers 
                    (@PathVariable String userId){
    List<Company> results = service.findCustomers(userId);
    return ResponseEntity.ok(results.asJava());
}

It will do output correctly, however the deal here will come with serialization. We need to try something else.

Approach 2. Custom Jackson mapper

The better solution is to use custom mapper. Hopefully, Vavr has its Jackson module. You need to add the follwoing dependency to build.gradle:

implementation 'io.vavr:vavr-jackson:0.10.2'

Next, register the custom mapper in the application class (in our case it is net.mednikov.cashtrack.CashtrackApplication.java):

@SpringBootApplication
public class CashtrackApplication {

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

	@Bean
    public ObjectMapper jacksonBuilder() {
        ObjectMapper mapper = new ObjectMapper();
        return mapper.registerModule(new VavrModule());
    }

}

Much better! Now test the customers endpoint again:

Image 6. Companies REST API – Vavr List problem solved

Congratulations! You completed first REST API endpoint. Next time we will implement a client side: creating and searching for companies. Don’t forget to follow me for updates. See you!

Copy link
Powered by Social Snap