What is the best way to do complex business logic computations for any API in Nestjs?

January 23, 2023 - 8 min read

The best way to do complex business logic computations for any API in Nestjs is to use a service Provider class for the Controller class of the API and delegate the complex computational tasks to the service Provider class. The service Provider class can be injected as a dependency to the Controller class.

To make a class a Provider or an injectable class, you can use the @Injectable decorator function from the @nestjs/common module and define it just above the Provider class declaration.

TL;DR

File: greet.service.ts

import { Injectable } from "@nestjs/common";

// 1. `GreetService` service `Provider` class
// will be responsible for handling the complex business
// logic computation for the `/greet` API endpoint.

// 2. The `@Injectable` decorator function instructs Nestjs
// runtime to treat the `GreetService` class as a `Provider`
// so that it can be injected as a dependency to
// the `GreetController` controller class
// and use the methods of this class.
@Injectable()
export class GreetService {
  sayHelloWorld() {
    return "Hello World";
  }
}

File: greet.controller.ts

import { Controller, Get } from "@nestjs/common";

// importing the `GreetService` provider class
import { GreetService } from "./greet.service";

@Controller("/greet")
export class GreetController {
  // declaring and initializing the `GreetService` provider
  // class using the fast and short way in the `constructor` method itself
  constructor(private greetService: GreetService) {}

  // The `Get()` decorator function will instruct the
  // Nestjs runtime to use the `sayHello()`
  // method for a `GET` of `/greet` endpoint.
  @Get()
  sayHello() {
    // using the `sayHelloWorld()` method from the
    // instance of the `GreetService` provider class.
    return this.greetService.sayHelloWorld();
  }
}

File: app.module.ts

import { Module } from "@nestjs/common";

import { GreetController } from "./greet/greet.controller";
import { GreetService } from "./greet/greet.service";

@Module({
  imports: [],
  controllers: [GreetController], // <- add controller here
  providers: [GreetService], // <- add provider here
})
export class AppModule {}

For example, let's say we have a GET API called /greet that returns the string of Hello World as the response.

The Controller class for the /greet API in Nestjs will look like this,

File: greet.controller.ts

import { Controller, Get } from "@nestjs/common";

@Controller("/greet")
export class GreetController {
  // The `Get()` decorator function will instruct the
  // Nestjs runtime to use the `sayHello()`
  // method for a `GET` of `/greet` endpoint.
  @Get()
  sayHello() {
    return "Hello World";
  }
}

NOTE: To know more about creating controllers in Nestjs, see the blog on How to make a simple GET request or an API endpoint in Nestjs?

If you look inside the sayHello() method of the GreetController class above, you can see that it is returning a simple string of Hello World. Let's imagine that returning the string is our complex business logic computation. Funny, Right? 😂.

Now let's move this logic to another class Called GreetService which will return the string of Hello World.

You may ask why are we moving this logic to another class from the GreetController class.

It is due to the separation of concerns which is a better design pattern. Consider a case where the logic is too big then it would become hard for us to test the results of the function and becomes too coupled with the API endpoint itself. To remove this hassle we can delegate more complex business logic computation to a service Provider class.

To do that first, we can make a class called GreetService to define all the service methods related to the /greet API endpoint.

It can be done like this,

File: greet.service.ts

// GreetService service `Provider` class
// will be responsible for handling the complex business
// logic computation for the `/greet` API endpoint.
export class GreetService {
  // cool code here!
}

Now we need to define the GreetService class as a Provider so that Nestjs runtime knows that it is a Provider class and can be injected into the GreetController controller class. To do that, we can use the @Injectable() decorator function from the @nestjs/common module and write it just before the GreetService class declaration.

It can be done like this,

File: greet.service.ts

import { Injectable } from "@nestjs/common";

// 1. `GreetService` service `Provider` class
// will be responsible for handling the complex business
// logic computation for the `/greet` API endpoint.

// 2. The `@Injectable` decorator function instructs Nestjs
// runtime to treat the `GreetService` class as a `Provider`
// so that it can be injected as a dependency to
// the `GreetController` controller class
// and use the methods of this class.
@Injectable()
export class GreetService {
  // cool code here!
}

We have made the GreetService class a Provider. Now we need to make a method inside the GreetService that returns the result of the complex business logic computation. In our case, it is the string of Hello World. Haha 😅.

Let's make a method called sayHelloWorld() inside the GreetService that returns the string Hello World.

It can be done like this,

File: greet.service.ts

import { Injectable } from "@nestjs/common";

// 1. `GreetService` service `Provider` class
// will be responsible for handling the complex business
// logic computation for the `/greet` API endpoint.

// 2. The `@Injectable` decorator function instructs Nestjs
// runtime to treat the `GreetService` class as a `Provider`
// so that it can be injected as a dependency to
// the `GreetController` controller class
// and use the methods of this class.
@Injectable()
export class GreetService {
  sayHelloWorld() {
    return "Hello World";
  }
}

We have finally made our small GreetService Provider. Yay 🥳!

Now we need to use the GreetService provider class's sayHelloWorld() method in the GreetController class.

To do that we need to inject the GreetService provider class as a dependency to the GreetController class. We can do this by initializing the GreetService class in the constructor method of the GreetController class. This process is called dependency injection.

A simple and fast way of doing the declaration and initialization of the GreetService class inside the GreetController class is by creating a private variable with the type of the GreetService class inside the parameters bracket.

It can be done like this,

File: greet.controller.ts

import { Controller, Get } from "@nestjs/common";

// importing the `GreetService` provider class
import { GreetService } from "./greet.service";

@Controller("/greet")
export class GreetController {
  // declaring and initializing the `GreetService` provider
  // class using the fast and short way in the `constructor` method itself
  constructor(private greetService: GreetService) {}

  // The `Get()` decorator function will instruct the
  // Nestjs runtime to use the `sayHello()`
  // method for a `GET` of `/greet` endpoint.
  @Get()
  sayHello() {
    return "Hello World";
  }
}

We have successfully injected the GreetService class. Now let's use the sayHelloWorld() method from the GreetService provider class and replace the return value of the sayHello() method of the GreetController class.

To use the instance of GreetService class which we have declared and initialized in the constructor, we can use the this object which will have access to the GreetService instance methods.

It can be done like this,

File: greet.controller.ts

import { Controller, Get } from "@nestjs/common";

// importing the `GreetService` provider class
import { GreetService } from "./greet.service";

@Controller("/greet")
export class GreetController {
  // declaring and initializing the `GreetService` provider
  // class using the fast and short way in the `constructor` method itself
  constructor(private greetService: GreetService) {}

  // The `Get()` decorator function will instruct the
  // Nestjs runtime to use the `sayHello()`
  // method for a `GET` of `/greet` endpoint.
  @Get()
  sayHello() {
    // using the `sayHelloWorld()` method from the
    // instance of the `GreetService` provider class.
    return this.greetService.sayHelloWorld();
  }
}

We have knitted together our GreetService provider class and the GreetController controller class together.

But this won't work as of now since Nestjs runtime doesn't have any context on the providers and the controllers.

For the Nestjs runtime to inject dependencies to appropriate classes (in our case GreetService provider class for the GreetController controller class) we have to add the controllers and providers in a referring Module class.

If you navigate to the app.module.ts (This is the default filename used if you have created the Nestjs project using the Nestjs CLI), there you need to import the GreetController class and the GreetService class and add it as a controller in the controllers array and as a provider in the providers array respectively in the @Module() decorator function.

It can be done like this,

File: app.module.ts

import { Module } from "@nestjs/common";

import { GreetController } from "./greet/greet.controller";
import { GreetService } from "./greet/greet.service";

@Module({
  imports: [],
  controllers: [GreetController], // <- add controller here
  providers: [GreetService], // <- add provider here
})
export class AppModule {}

We have finally knitted everything together and created a provider class to delegate complex business logic computations for the /greet API endpoint. Yay 🥳!

See the above code live in codesandbox.

To see the API response go to https://ni39xu.sse.codesandbox.io/greet.

That's all 😃.