Building a Robust API: Structuring Controllers, Entities, DTOs, and Modules

Abdullah Irfan
12 min readOct 25, 2023

--

Photo generated by Bing

This is third article of series Building a Robust Backend: A Comprehensive Guide Using NestJS, TypeORM, and Microservices. Our purpose is to build an email sync system for Gmail, so let’s start setting up its Skelton. We will first define a table for storing our email accounts, the column names and description of each column name is:

  • full_name: Represents the full name of the user associated with the Gmail account. It’s essential to identify and address the user properly in any interface or communication, and it will be set to not allow nulls, ensuring that every record has a name.
  • email: Represents the Gmail address or email ID of the user. This is crucial as it serves as the primary identifier for the Gmail account in question, allowing for operations like sending emails, accessing user data, etc. Like the name, it will not allow nulls, ensuring that every record must have an associated email.
  • access_token: Used for authentication and authorization purposes. It’s a token granted by Gmail (Google) that allows our application to access the user’s Gmail account on their behalf without needing their password. This token has a limited lifespan.
  • refresh_token: When the access_token expires, the refresh_token can be used to obtain a new access_token. It's a more long-lived token and ensures that our application can maintain access to the user's Gmail account without repeatedly asking the user to grant permissions.
  • token_type: Specifies the type of token being used. In most OAuth2 implementations, this will typically be “Bearer”. It’s a part of the standard OAuth2 specification and informs the client how the access_token should be used in API requests.
  • scope: Defines the level or type of access that the application has to the user’s Gmail account. For instance, it might specify that the app can read emails but not send them, or it might state the app can access Google Drive documents. The scope is set during the OAuth2 authorization process and can vary based on what permissions the user granted.
  • expiry_date: Represents the timestamp (usually in milliseconds since the Unix epoch) when the access_token will expire. This is important for the application to know when it needs to refresh the access_token using the refresh_token.

Each of these columns plays a crucial role in enabling and managing the OAuth2 flow, particularly when integrating with services like Gmail. They ensure that the application can access the Gmail account with the appropriate permissions and continue to do so over time without constant user intervention.

For setting this table up, we will setup entities. For those who are new or haven’t worked with ORM’s yet, let's get some theoretical knowledge about entities first. In the context of NestJS and many other ORMs (Object-Relational Mapping) tools, entities represent tables in a database. They are TypeScript (or JavaScript) classes that define and describe the structure of the table, its columns, and the relationships between different tables.

Here are some key points about entities in NestJS when using TypeORM:

  1. Database Table Representation: Each entity corresponds to a table in the database. The name of the table can be specified using the @Entity() decorator.
  2. Columns and Data Types: Entity properties decorated with @Column() define columns in the table. The data type, constraints, and other column-specific settings can be configured within the @Column() decorator.
  3. Primary Keys: Using the @PrimaryGeneratedColumn() decorator, we can define a primary key column with auto-incrementing values.
  4. Relations: Entities can define relationships with other entities (e.g., One-to-One, One-to-Many, Many-to-One, Many-to-Many) using decorators like @OneToOne(), @OneToMany(), @ManyToOne(), and @ManyToMany().
  5. Synchronization: With TypeORM and other ORMs integrated with NestJS, entities can be synchronized with the database. This means that the database schema can be automatically created or updated based on the entity definitions.
  6. Query Repository: Entities are also used with repositories in TypeORM. A repository is a class that can run database queries for a specific entity. For example, a repository for a User entity can run queries to find, insert, update, or delete users.
  7. Encapsulation: Entities often include both the data structure (columns) and certain methods or functions that operate on that data. This encapsulates the behaviour and structure of the data model, following the principles of Object-Oriented Programming (OOP).

For our scenario, let’s create new folder in src, gmail-account and inside it creates a folder named entities. Though conventionally we don’t make separate folders for entities and DTOs however I prefer it this way because an API resource can have multiple entities and DTOs for different scenarios. Now inside entities folder create a file named gmail-account.entity.ts and paste the following code (it’s just defining column names, their type, and whether they are nullable or not) in the file.

import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity('gmail-accounts')
export class GmailAccount {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column({ type: 'varchar' })
full_name: string;

@Column({ type: 'varchar' })
email: string;

@Column({ type: 'varchar', nullable: true })
access_token?: string;

@Column({ type: 'varchar', nullable: true })
refresh_token?: string;

@Column({ type: 'varchar', nullable: true })
token_type?: string;

@Column({ type: 'varchar', nullable: true })
scope?: string;

@Column({ type: 'bigint', nullable: true })
expiry_date?: number;
}

Now we have our database ready to accept the data, however, we haven’t setup how to receive this data. We will be using Data Transfer Objects (DTOs) for this. For those who are new or who haven’t used it before, DTOs (Data Transfer Objects) are objects that define how the data will be sent over the network. In the context of NestJS and many other frameworks, DTOs often serve multiple purposes:

  1. Shape Validation: They can define and enforce the shape of the data that’s expected in requests (e.g., POST or PUT requests). This helps ensure that the incoming data adheres to a specific structure.
  2. Type Safety: When using TypeScript, DTOs provide type definitions for our data, aiding in type safety, autocompletion, and error checking during development.
  3. Data Transformation: In more complex applications, DTOs can also help in transforming data from one format to another when sending or receiving data.

However, this doesn’t ensure that which variables are required or optional, what should be their type, etc. For that, we will use class validators provided by NestJS. NestJS integrates seamlessly with the class-validator package, which allows validation of the properties of an object based on specified decorators. With Class Validators:

  1. Decorator-based Validation: We can use decorators like @IsEmail(), @IsString(), @Length(), etc., on the properties of our DTOs to specify validation rules.
  2. Automatic Validation: When set up correctly with NestJS, the framework will automatically validate incoming requests against the DTO and its validation rules. If the incoming data does not match the expected format or violates any validation rules, NestJS can automatically return an error response.
  3. Custom Validation: Besides the built-in validation decorators, we can also create custom validation rules to suit specific needs.
  4. Error Messages: Validation decorators can also include custom error messages that will be returned if a specific validation rule is violated.

Class validator isn’t pre-provided in NestJS and we need to install it by:

npm i --save class-validator

For our scenario, we will be creating two DTOs, one for creating the record and other for updating the record. But the update record DTO will have same values as create but this time they aren’t necessarily required. So for our ease we will be using a utility function from another package. In NestJS, PartialType is a utility function provided by the @nestjs/mapped-types package. It's used to create new types based on an existing type but with all of its properties set as optional. This package is installed by:

npm i --save @nestjs/mapped-types

Now we are all setup, lets create the two DTOs.

// create-gmail-account.dto.ts
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';

export class CreateGmailAccountDTO {
@IsString()
@IsNotEmpty()
full_name: string;

@IsEmail()
email: string;
}
// update-gmail-account.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateGmailAccountDTO } from './create-gmail-account.dto';

export class UpdateGmailAccountDTO extends PartialType(CreateGmailAccountDTO) {}

We have database to store data and DTOs to receive data from network with proper data validation. So now we need logic building to manipulate the sanitized data we receive and perform operation accordingly, usually services are responsible for these kinds of operations. In NestJS, a service is typically a TypeScript class that encapsulates business logic, data manipulations, database interactions, external API calls, and other backend-related operations. Services play a vital role in the architecture of a NestJS application by promoting the separation of concerns, modularity, and testability.

Here are some key points about services in NestJS:

  1. Single Responsibility: A well-designed service should have a clear, single responsibility. For example, a UserService might handle operations related to users, like fetching a user from the database, updating user details, etc.
  2. Dependency Injection: NestJS follows the Dependency Injection (DI) design pattern. Services are commonly injected into other components (like controllers or other services) through constructors. This makes it easier to manage instances, mock services for testing, and ensure modularity.
  3. Providers: In the context of NestJS, services are often referred to as providers. They can be registered in a module's providers array, making them available for injection in other parts of the application.
  4. Testability: One of the advantages of separating out business logic into services is that it makes unit testing easier. When services are designed with DI in mind, we can easily mock dependencies and test the service in isolation.
  5. Reusability: Services can be reused across different parts of our application. For instance, if both a controller and a gateway (in the case of WebSockets) need to access user data, they can both utilize the same UserService.
  6. Decorators: While services primarily contain business logic, they can also utilize various decorators provided by NestJS or other integrated libraries to enhance their functionalities.

Since at the moment, our service is limited to creating and updating records, so its code will be:

// gmail-account.controller.ts
import {
Controller,
Post,
Body,
Patch,
Param,
ValidationPipe,
UsePipes,
} from '@nestjs/common';
import { GmailAccountService } from './gmail-account.service';
import { CreateGmailAccountDTO } from './dtos/create-gmail-account.dto';
import { UpdateGmailAccountDTO } from './dtos/update-gmail-account.dto';

@UsePipes(ValidationPipe)
@Controller('gmail-accounts')
export class GmailAccountController {
constructor(private readonly gmailAccountService: GmailAccountService) {}

@Post()
async create(@Body() dto: CreateGmailAccountDTO) {
return await this.gmailAccountService.create(dto);
}

@Patch(':id')
async update(@Param('id') id: string, @Body() dto: UpdateGmailAccountDTO) {
return await this.gmailAccountService.update(id, dto);
}
}

Finally we have almost everything, all that’s left is defining a control method to control the requests. In NestJS, a controller handles incoming HTTP requests and returns responses to the client. Controllers play a pivotal role in determining how our application responds to user interactions or API calls.

Here are some key points about controllers in NestJS:

  1. Routing: At the core, controllers are responsible for defining the routes of our application. Using various decorators, we can specify the paths and HTTP methods (like GET, POST, PUT, DELETE) for each handler method within a controller.
  2. Decorators: NestJS provides a set of decorators (like @Get(), @Post(), @Body(), @Param(), etc.) that make it intuitive to define routes, fetch data from the request, and even validate incoming data.
  3. Separation of Concerns: Controllers in NestJS should primarily focus on handling HTTP-specific tasks. Business logic, database operations, and other complex tasks should be delegated to services. This ensures a clear separation of concerns and enhances maintainability.
  4. Data Transformation: While services return raw data or perform operations, controllers can transform this data into the desired shape or format before sending it as a response.
  5. Error Handling: Controllers can also handle errors. With the help of NestJS’s exception filters, we can capture exceptions and transform them into user-friendly error messages or HTTP responses.
  6. Dependency Injection: Like services, controllers in NestJS benefit from the framework’s robust dependency injection system. This means we can easily inject services, repositories, or other providers into controllers to utilize their functionalities.

For our scenario, the controller (API) will be:

// gmail-account.controller.ts
import { Controller, Post, Body, Patch, Param } from '@nestjs/common';
import { GmailAccountService } from './gmail-account.service';
import { CreateGmailAccountDTO } from './dtos/create-gmail-account.dto';
import { UpdateGmailAccountDTO } from './dtos/update-gmail-account.dto';

@Controller('gmail-accounts')
export class GmailAccountController {
constructor(private readonly gmailAccountService: GmailAccountService) {}

@Post()
async create(@Body() dto: CreateGmailAccountDTO) {
return await this.gmailAccountService.create(dto);
}

@Patch(':id')
async update(@Param('id') id: string, @Body() dto: UpdateGmailAccountDTO) {
return await this.gmailAccountService.update(id, dto);
}
}

We have almost everything ready, now all that’s left is to let application know about it by setting up module. In NestJS, a module is a fundamental organizational unit, which allows us to group related features together. Modules help in structuring and organizing the application in a modular and maintainable way, ensuring that related capabilities are encapsulated within specific boundaries.

Here are some key points about modules in NestJS:

  1. Encapsulation: Modules encapsulate a block of related functionalities. For example, everything related to users (like services, controllers, providers, etc.) can be organized within a UserModule.
  2. Imports & Exports: One of the main features of modules is the ability to import other modules. If a module needs functionalities provided by another module, it can simply import it. Modules can also define exports, which are a set of providers that should be available to other modules that import it.
  3. Singletons: All providers (like services) defined in a module are instantiated as singletons. This ensures that we get the same instance of a service across our application, enabling efficient data sharing and caching.
  4. Application Organization: As NestJS applications grow, modules become essential for maintaining a clean organizational structure. we can split our application into feature modules, shared modules, core modules, etc.
  5. Lazy Loading: NestJS allows modules to be lazily loaded. This can help in splitting the application into multiple smaller parts that are loaded only when needed, enhancing performance, especially in microservices architectures.
  6. Decorators & Metadata: The @Module() decorator is used to define a module and its metadata. The metadata can include imports, controllers, providers, and exports.

Thus, our module code will be:

// gmail-account.module.ts
import { Module } from '@nestjs/common';
import { GmailAccountController } from './gmail-account.controller';
import { GmailAccountService } from './gmail-account.service';

@Module({
controllers: [GmailAccountController],
providers: [GmailAccountService],
})
export class GmailAccountModule {}

Lastly, we need to tell the application that we have a new module available by importing it in app module. Our update app module will be:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { GmailAccountModule } from './gmail-account/gmail-account.module';

@Module({
imports: [
GmailAccountModule,
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'postgres',
password: 'admin',
database: 'gmail-sync',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

Let’s run our application by npm start , when the application starts, its logger on CLI should show google-accounts module loaded and its endpoints mapped from router explorer as shown in image below.

Logger image showing module loaded

On hitting POST request from postman, it should respond with records created, and DB should have this record in it (both images shown below).

Postman API call and response
DB record created

So, till now we have basic setup ready, but it has some things missing. It doesn’t have any service logic to interact with Gmail. Another thing, about entities, if we change any entity type, it will crash our application due to mismatch type, like if I change full_name to Boolean instead of String, it will show error (image below).

Error on type change

So to deal with it, we will introduce migrations in our next story. As usual, this story code is available on GitHub in feature/users-gmail-accounts branch. If you appreciate this work, please show your support by clapping for the story and starring the repository.

Before we conclude, here’s a handy toolset you might want to check out: The Dev’s Tools. It’s not directly related to our tutorial, but we believe it’s worth your attention. The Dev’s Tools offers an expansive suite of utilities tailored for developers, content creators, and digital enthusiasts:

  • Image Tools: Compress single or multiple images efficiently, and craft custom QR codes effortlessly.
  • JSON Tools: Validate, compare, and ensure the integrity of your JSON data.
  • Text Tools: From comparing texts, shuffling letters, and cleaning up your content, to generating random numbers and passwords, this platform has got you covered.
  • URL Tools: Ensure safe web browsing with the URL encoder and decoder.
  • Time Tools: Calculate date ranges and convert between Unix timestamps and human-readable dates seamlessly.

It’s a treasure trove of digital utilities, so do give it a visit!

--

--