Creating a Scalable GraphQL Endpoint in NestJS: Best Practices and Implementation

 In this article, I will demonstrate how to create a sample GraphQL endpoint using NestJS as the backend service.

Choosing between REST and GraphQL comes with its own set of pros and cons, and the best option depends on your specific requirements. This article focuses on building a scalable and flexible NestJS backend with GraphQL while incorporating best practices. So do not expect the final output to be a simple CRUD example.

Create a NestJS application using NestCLI

Nest CLI can be used to create NestJS applications simply.

$ npm i -g @nestjs/cli
$ nest new project-name

npm libraries

In this application I will use the following libraries:

npm i dotenv
npm i @nestjs/graphql @nestjs/apollo @apollo/server graphql --force
npm i --save @nestjs/typeorm typeorm --force
npm i pg --save --force

Structure

You can use the Nest CLI to generate modules, but in this article, I will manually create the folder structure and classes for better control and customization. Here is the folder structure that I am going to use.

  • common — To keep common classes like base.model, base.repository, base.service…etc.
  • config — To keep config files
  • modules — To keep modules separately. Each module contains graphql, service, repository, and models directories.
  • util — To keep utility classes

Use environment variables with dotenv

Read this article for more details about using dotenv with NestJS.

Create the database module with multiple database support.

Instead of creating single DB support, here I will use how to make your application with multiple database support. Because this might be important in real-world applications. I am using Postgres as the DB.

Create a Postgres DB connection

import { TypeOrmModuleOptions } from "@nestjs/typeorm";
import { EnvUtils } from "src/util/env.utils";

export const DBS = {
CLIENT: EnvUtils.get('CLIENT_DB'),
STORES: EnvUtils.get('STORES_DB')
};

const COMMON_DB = {
host: EnvUtils.get('HOST'),
port: 5432,
username: EnvUtils.get('USERNAME'),
password: EnvUtils.get('PASSWORD'),
synchronize: false,
autoLoadEntities: true,
logging: false,
}

export const dbConnection: TypeOrmModuleOptions[] = [
{
type: 'postgres',
...COMMON_DB,
name: DBS.CLIENT,
database: DBS.CLIENT
},
{
type: 'postgres',
...COMMON_DB,
name: DBS.STORES,
database: DBS.STORES
}
]

defines shared settings for all PostgreSQL connections:

  •  — database credentials.
  •  — Prevents TypeORM from auto-creating tables (safe for production).
  •  — Automatically loads entity classes (e.g., Client) from your codebase.
  • — Disables query logging.

dbConnection is an array of TypeOrmModuleOptions, defining two PostgreSQL connections:

  • One for the CLIENT database (named and using DBS.CLIENT).
  • One for the STORES database (named and using DBS.STORES).

Create database module

import { DynamicModule, Global, Module } from "@nestjs/common";
import { TypeOrmModule, TypeOrmModuleOptions } from "@nestjs/typeorm";

@Global()
@Module({})
export class DatabaseModule{
static forRoot(connection: TypeOrmModuleOptions[]): DynamicModule{
return{
module: DatabaseModule,
imports: connection.map((connection) =>
TypeOrmModule.forRoot(connection),
),
exports:[TypeOrmModule]
};
}
}

This will dynamically register multiple database connections.

  • Global Scope:

ensures that once DatabaseModule is imported (e.g., in AppModule), its providers and exports are available throughout the application without needing to import it elsewhere.

  • Dynamic Module:

is a static method that returns a DynamicModule. This allows you to pass an array of TypeORM connection options (like dbConnection) and configure multiple database connections.

  • TypeORM Integration:

iterates over the provided connections (e.g., dbConnection) and registers each one with TypeORM using forRoot. Each connection becomes a named connection in the app.

  • Exports:

makes the TypeOrmModule (and its providers, like repositories) available to other modules that need database access.

Create base.model.ts

This abstract class serves as a common base for all model classes, eliminating duplicate fields and ensuring cleaner, more maintainable code. You can add more common fields according to your requirements.

import { Field, ObjectType } from "@nestjs/graphql";
import { Column, CreateDateColumn, PrimaryGeneratedColumn } from "typeorm";

@ObjectType()
export abstract class BaseModel {

@Field()
@PrimaryGeneratedColumn('uuid')
id?: string;

@Field()
@CreateDateColumn({
type: 'timestamp',
name: 'created_at',
default: () => "CURRENT_TIMESTAMP(6)"
})
createdAt?: Date;

@Field({ nullable: true })
@Column({
name: 'created_by',
nullable: true,
type: 'uuid'
})
createdBy?: string;

@Field()
@CreateDateColumn({
type: 'timestamp',
name: 'updated_at',
default: () => "CURRENT_TIMESTAMP(6)"
})
updatedAt?: Date;

@Field({ nullable: true })
@Column({
name: 'updated_by',
nullable: true,
type: 'uuid'
})
updatedBy?: string;
}

There are some decorators like and  which are specific to GraphQL.

Create base.service.ts

For the common services, this serves as a generic base class for services. If you are expecting to have some common services, you can add them to this class.

import { Logger } from "@nestjs/common";
import { BaseRepository } from "../repositories/base.repository";
import { BaseModel } from "../models/base.model";

export class BaseService<T extends BaseModel> {
constructor(
protected readonly logger: Logger,
protected readonly repository: BaseRepository<T>
) {
this.logger.debug(`${this.constructor.name} initialized`);
}

async findAll(): Promise<T[]> {
this.logger.debug('Fetching all entities');
try {
const entities = await this.repository.find();
this.logger.debug(`Found ${entities.length} entities`);
return entities;
} catch (error) {
this.logger.error('Error fetching entities', error.stack);
throw error;
}
}

}

Create base.repository.ts

This is the common repository where you can define shared functions for reuse.

import { Logger } from "@nestjs/common";
import { Repository, FindOneOptions, FindManyOptions } from "typeorm";
import { BaseModel } from "../models/base.model";

export class BaseRepository<T extends BaseModel> {
constructor(
protected readonly logger: Logger,
protected readonly repository: Repository<T>
) {
this.logger.debug(`${this.constructor.name} initialized`);
}

/**
* Retrieves all entities matching the given options.
* @param options - TypeORM find options (e.g., where, order).
* @returns A promise resolving to an array of entities.
*/

async find(options?: FindManyOptions<T>): Promise<T[]> {
this.logger.debug('Fetching entities', options);
try {
const entities = await this.repository.find(options);
this.logger.debug(`Found ${entities.length} entities`);
return entities;
} catch (error) {
this.logger.error('Error fetching entities', error.stack);
throw error;
}
}

Adding common functions to the base service and base repository depends on your specific needs. I can’t definitively say it must be done this way.

Create the client module.

Let’s manually create our first module instead of using the Nest CLI. The  module will include directories for modelsservicesrepositories, and graphql. The GraphQL-related queries and mutations will be organized within the  directory.

Create a model

This is my model class for I have added a few sample fields. Since it extends  all common fields from the base class are also available in this model.

import { Field, ObjectType } from "@nestjs/graphql";
import { BaseModel } from "src/common/models/base.model";
import { Column, Entity } from "typeorm";

@ObjectType()
@Entity('client')
export class Client extends BaseModel{

@Field({nullable: true})
@Column({
name: 'client_name',
type:'text',
nullable: true
})
client_name: string;

@Field({nullable: true})
@Column({
name: 'client_code',
type:'text',
nullable: true
})
client_code: string;

@Field({nullable: true})
@Column({
name: 'client_description',
type:'text',
nullable: true
})
client_description: string;

@Field()
@Column({
name: 'is_active',
type:'boolean',
})
is_active: boolean;
}

Create a service

Here, I’ve created a service interface and a service class. While it may not be strictly required, this interface pattern becomes useful in large-scale applications, enhancing testability, modularity, and the flexibility to swap implementations when needed.

import { Client } from "../models/client.model";

export abstract class ClientServiceInterface{
abstract getClients(): Promise<Client[]>;
}
import { Injectable, Logger } from "@nestjs/common";
import { ClientServiceInterface } from "./client.service.interface";
import { Client } from "../models/client.model";
import { ClientRepositoryInterface } from "../repositories/client.repository.interface";
import { BaseService } from "src/common/services/base.service";

@Injectable()
export class ClientService extends BaseService<Client> implements ClientServiceInterface{

constructor(private readonly clientRepository: ClientRepositoryInterface){
super(
new Logger(ClientService.name),
clientRepository,
);
}

async getClients(): Promise<Client[]> {
this.logger.log(ClientService.name);
return await this.clientRepository.getClients();
}
}

You can implement your own services while also leveraging the benefits of extending the base service.

Create a repository

You can use the same pattern for the repositories as well.

import { BaseRepository } from "src/common/repositories/base.repository";
import { Client } from "../models/client.model";

export abstract class ClientRepositoryInterface extends BaseRepository<Client>{

abstract getClients(): Promise<Client[]>;
}
import { BaseRepository } from "src/common/repositories/base.repository";
import { Client } from "../models/client.model";
import { Injectable, Logger } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { DBS } from "src/config/database/postgres.db.connection";
import { Repository } from "typeorm";

@Injectable()
export class ClientRepository extends BaseRepository<Client>{

constructor(
@InjectRepository(Client, DBS.CLIENT)
private readonly clientRepository: Repository<Client>,
){
super(new Logger(ClientRepository.name), clientRepository)
}

async getClients(): Promise<Client[]>{
const result = await this.clientRepository.find();
return result || [];
}
}

Since we are dealing with multiple DBs you need to specify the DB when injecting the repository.

Create the client resolver

GraphQL includes queries for fetching data and mutations for modifying data. In this example, I’ve created only a resolver for demonstration purposes, but we’ll add mutations later.

import { Query, Resolver } from "@nestjs/graphql";
import { Client } from "../models/client.model";
import { ClientServiceInterface } from "../service/client.service.interface";
import { InternalServerErrorException } from "@nestjs/common";

@Resolver(() => Client)
export class ClientResolver{

constructor(private readonly clientService: ClientServiceInterface){}

@Query(()=> [Client], {name: 'clients', nullable: false}, )
async getClients(): Promise<Client[]>{
try{
return await this.clientService.getClients();
}catch(e){
throw new InternalServerErrorException('Failed to fetch clients');
}
}
}

This is an example of a basic resolver.

  • @Resolver(() => Client) — This is the resolver declaration, which indicates this class handles GraphQL operations related to the Client type.
  • @Query(()=> [Client], {name: ‘getClients’, nullable: false}) — This query definition defines a GraphQL query named getClients that returns a non-nullable array of Client objects.

Create client module

Now I will create the client module manually since I didn’t use CLI.

import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Client } from "./models/client.model";
import { DBS } from "src/config/database/postgres.db.connection";
import { ClientRepositoryInterface } from "./repositories/client.repository.interface";
import { ClientRepository } from "./repositories/client.repository";
import { ClientServiceInterface } from "./service/client.service.interface";
import { ClientService } from "./service/client.service";
import { ClientResolver } from "./graphql/client.resolver";

@Module({
imports:[TypeOrmModule.forFeature([Client], DBS.CLIENT)],
providers:[
{
provide: ClientRepositoryInterface,
useClass: ClientRepository,
},
{
provide: ClientServiceInterface,
useClass: ClientService,
},
ClientResolver,
],
exports:[ClientServiceInterface],
}
)


export class ClientModule{}

As you can see, there are imports, providers, and exports as usual. There are a few more things you need to know. Here I used interface-based injection, which is more flexible.

I am using DBS.CLIENT as the connection name, which implies you might have multiple database connections (e.g., DBS.AUTH, DBS.ORDER). This is great for microservices or separation of concerns.

Exporting ClientServiceInterface is good for modularity, but ensure other modules actually need it. If only the resolver within this module uses it, you might not need to export anything.

Update app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ApolloDriverConfig, ApolloDriver } from '@nestjs/apollo';
import { GraphQLModule } from '@nestjs/graphql';
import { ClientModule } from './modules/client/client.module';
import { DatabaseModule } from './config/database/database.module';
import { dbConnection } from './config/database/postgres.db.connection';


@Module({
imports: [
DatabaseModule.forRoot(dbConnection),
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: true,
playground: false
}
),ClientModule],
controllers: [AppController],
providers: [AppService],
}
)

export class AppModule {}

Now we need to update the app.module.ts file. Here is the breakdown.

  • Database Setup

DatabaseModule.forRoot(dbConnection) — Initializes the database connection(s) using a custom DatabaseModule.

  • GraphQL Setup

GraphQLModule.forRoot<ApolloDriverConfig>(…) - Configures GraphQL using the Apollo driver.

driver: ApolloDriver — Uses Apollo Server under the hood.

autoSchemaFile: true — Automatically generates the GraphQL schema (e.g., schema.gql) based on your resolvers and types.

playground: false — Disables the GraphQL Playground UI (you’d need a tool like Postman or a custom client to query the API).

  • Feature Modules:

ClientModule — Imports your ClientModule, which brings in the ClientResolver, ClientService, and ClientRepository for client-related functionality.

Setup DB

Since I use Postgres, I created a DB called ClientDB and created a table called client. You can make it according to your model. And also do not forget to update the .env file.

PORT=3002

# DB connection
HOST=127.0.0.1
DB_PORT=5432
USERNAME=<username>
PASSWORD=<password>

# DBs
CLIENT_DB=clientDB
STORES_DB=storesDB

Test the application

Since I’ve disabled the playground, I access the endpoints via Postman. You can create a graphql request, and the url will be http://localhost:3002/graphql because I am using port 3002.

Creating a Scalable GraphQL Endpoint in NestJS: Best Practices and Implementation Creating a Scalable GraphQL Endpoint in NestJS: Best Practices and Implementation Reviewed by Ravi Yasas on 11:54 PM Rating: 5

No comments:

Powered by Blogger.