DEV Community

Husnain Mustafa
Husnain Mustafa

Posted on

Apollo GraphQL Implementation in NestJs

Today, we will be learning how we can implement the very basics of Apollo GraphQL in NestJs.

What is GraphQL and why is GraphQL?

GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data (definition from https://graphql.org/). Before graphQL most web application (still) replies on REST Api as a channel between backend and frontend.
The drawbacks of REST Api are:

  1. Over-fetching or Under-fetching: REST Api always sends back the same data structure for a given url. It could result in over-fetching, i.e getting more data than required by frontend, or under-fetching, i.e getting less data than required by frontend.

  2. Multiple endpoints: Usually REST Api has multiple points, one for each type of data structure.

GraphQL solves both of these problems by:

  1. Allowing the client, frontend, to fetch the only data that is required. Frontend sends the structure of the data along with the request. And GraphQL only sends back the requested data.

  2. GraphQL provide single endpoint, usually over /graphql route. Every query or mutation request is sent to this endpoint.

Now we have some idea what GraphQL is. Let's start implementing a GraphQL in NestJS.

Setting up the project

  1. First, we need to have nestjs/cli installed. Here is the command that will install the nestjs/cli: npm i -g @nestjs/cli

  2. Once, we have installed nestjs cli, we can create a nest project by nest new <project-name>.

  3. Then it will ask you the package manager that you want to use for this project, as shown below:

Image description

You can select any of them. For this project, I will go with npm. Now it will install some packages and make the directory structure for you. Once done, you would have the following directory structure:

Image description

Our main focus of work would be /src directory. Before moving on, lets understand some basics of the files that you find inside /src directory:

main.ts is the root file and it is where the execution of project begins.

app.controller.spec.ts is the test file. *.spec.ts are used as tests. This would not be covered in this article. So we can delete it.

app.controller.ts is the Controller file. Controllers are one which receives the incoming request, sends the request to Service, and returns the response backed to client. Controllers are the file where we define the api route and method for a specific data. You can read more about Controllers here.
But we are not going to use Controllers in our GraphQL server as Graphql uses Resolver (described later) rather than Controller.

app.service.ts is the Service. Services are responsible to handle all business logic for a particular data. It is usually responsible to fetch/alter the data from the database.

app.module.ts defines the module. In nestJs, Everything comes under some module. Whole code is sliced down in different modules. It is responsible to configure the structure of the application.

  1. Installing GraphQL We can have either have Express or Fastify as our web framework. For this tutorial, we would go with Express. Below is the command to install apollo graphQL: npm i @nestjs/graphql @nestjs/apollo @apollo/server graphql

Configuring Apollo GraphQL:

Open /src/app.module.ts and add GraphQL Module in imports like:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';

@Module({
  imports: [GraphQLModule.forRoot<ApolloDriverConfig>({
    driver: ApolloDriver,
    autoSchemaFile: true,
  })],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

The configuration of GrapqhQL, which is provided as an object in forRoot:

{
    driver: ApolloDriver,
    autoSchemaFile: true,
}
Enter fullscreen mode Exit fullscreen mode

You can read more about the configuration here

You might not be in the right directory. If not, move to your project directory by:
cd <project-name>

Now you can verify that graphQL server has been configured successfully by running the nest server by the following command:
nest start

and open a browser and go to this url:
localhost:3000/graphql

You would see the grapqQL playground:

Image description

Playground is a place where you can test your APIs. We will use playground later in this tutorial when we will be done making some Queries and Mutations in Nest GraphQL Server.

Implementing GraphQL

Now its time we start making GraphQL APIs. For that, we need to have some use case. So for this tutorial, we are going to have 2 entities, Brands and their related Products.

Brands

Module

First we are going to implement Brands module. So to generate a module, you can type the following command in terminal:
nest g module brands

This command will create a Brands directory inside /src with 'brands.module.ts' file.

Model

Now we want Brands model. Which will be a schema for Brands. There are 2 ways to have a schema, Schema First (through Schema Definition Language, SDL) and Code First. Lets pause the tutorial and understand these two first.

Difference between Schema First and Code First

These both approaches are used to make a schema. In Schema First, we define the schema, using SDL.
Schema first approach looks like this:

type Brands {
  id: Int!
  name: String!
  logo: String
  type: String
}
Enter fullscreen mode Exit fullscreen mode

The same thing can be done through Code First by:

import { Field, Int, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class Brands {
  @Field(() => Int)
  id: number;

  @Field()
  name: string;

  @Field({nullable: true})
  logo?: string;

  @Field({nullable: true})
  type?: string;
}

Enter fullscreen mode Exit fullscreen mode

Here we are taking help from nestjs/graphql package which provides the required decorators and types.

Lets go through each thing one by one.

@ObjectType() decorator

@ObjectType() is used to define structure of the data. Here we are defining the structure of Brands which will have the above attribute.

@Field() decorator

@Field() is used to define a single attribute of the data. It has multiple over-loaders, one of which is takes a function as first parameter, like in the first field defined above:
@Field(() => Int)
Here we are defining type of the id attribute would be Integer.

But why? We haven't define the type of other fields in the @Field decorator, then why this? Well in TS, type number could be both integer and float. But in GraphQL, an attribute could either be Int or Float. So we have to explicitly tell Nest, that this field is of type Int.

We do not have to tell Nest explicitly about the string type. That is because string in TS corresponds to String in GraphQL.

We can also provide an object in the @Field decorator as:
@Field({nullable: true, defaultValue: "default"})
Here you can define multiple things like whether the attribute can be null and what would be default value for that attribute. You can read more about @Field decorator here.

Back to Model

Now we need to make a model file in our /src/brands directory named:
brands.model.ts:

import { Field, Int, ObjectType } from '@nestjs/graphql';

@ObjectType()
export class Brands {
  @Field((type) => Int)
  id: number;

  @Field({ nullable: true })
  name?: string;
}
Enter fullscreen mode Exit fullscreen mode

For now, we are going with two fields only. Later we will add another field, products, which link each brand to their products.

Service

Now we also want Brands service so we can handle all the business logic. To generate a service, here is the command:
nest g service brands.

You would see 'brands.service.ts' in /src. We will alter that file to:
brands.service.ts:

import { Injectable } from '@nestjs/common';
import { Brands } from './brands.model';

@Injectable()
export class BrandsService {
  private readonly brands: Brands[] = [
    { id: 1, name: 'Adidas' },
    { id: 2, name: 'Nike' },
  ];
  findById(id: number): Brands {
    return this.brands.find((brand) => brand.id == id);
  }
  findAll(): Brands[] {
    return this.brands;
  }
  addBrand(id: number, name: string): Brands {
    this.brands.push({ id, name });
    return { id, name };
  }
}
Enter fullscreen mode Exit fullscreen mode
@Injectable() decorator

@Injectable() is used to let Nest know that the class we are defining is injectable to other providers and controllers. By this, we can access the class instance through other classes.

In Brands class, we have defined a variable brands. Usually this is to be done through database. Database is responsible for storing such data. But as we are not going to use any database in this tutorial, we are storing the data within the app.

Furthermore, we have defined some function so fetch from and add to brands array.

Resolver

Rather than using Controller, which defines the API, we use Resolver in GraphQL. So make a file in /src/brands:
brands.resolver.ts:

import {
  Args,
  Int,
  Mutation,
  Query,
  Resolver,
} from '@nestjs/graphql';
import { Brands } from './brands.model';
import { BrandsService } from './brands.service';

@Resolver(() => Brands)
export class BrandsResolver {
  constructor(
    private brandsService: BrandsService,
  ) {}

  @Query(() => [Brands])
  getAllBrands(): Brands[] {
    return this.brandsService.findAll();
  }

  @Query(() => Brands)
  getBrandById(@Args('id', { type: () => Int }) id: number): Brands {
    return this.brandsService.findById(id);
  }

  @Mutation(() => Brands)
  addBrand(
    @Args('id', { type: () => Int }) id: number,
    @Args('name') name: string,
  ): Brands {
    return this.brandsService.addBrand(id, name);
  }
}
Enter fullscreen mode Exit fullscreen mode
@Resolver() decorator

@Resolver decorator is used to define a resolver, a class which defines GraphQL Queries and Mutations, which client can use for requests. In @Resolver, we need to pass a function as the first parameter which returns the Object that this Resolver would resolve, in our case, it's Brands.
@Resolver(() => Brands)

In the class BrandsResolver, we defined a constructor. Although we are not doing anything inside the block of constructor, but we are defining the BrandsService as brandsService by:

constructor(
    private brandsService: BrandsService,
  ) {}
Enter fullscreen mode Exit fullscreen mode

Here we are injecting our BrandsService that we made before with @Injectable()

Next we are defining a GraphQL Query by:

@Query(() => [Brands])
  getAllBrands(): Brands[] {
    return this.brandsService.findAll();
  }
Enter fullscreen mode Exit fullscreen mode
@Query() decorator

@Query is like the GET Api of REST, it takes first parameter as function which returns the type of the return type of request. In the above Query, we are returning Brands array back to the client when client calls this API.
Below it, we define a function, in which we fetch the Brands by using the BrandsService. Name of the function will be used as name of the Query.

After the queries, we are defining a mutation:

@Mutation(() => Brands)
  addBrand(
    @Args('id', { type: () => Int }) id: number,
    @Args('name') name: string,
  ): Brands {
    return this.brandsService.addBrand(id, name);
  }
Enter fullscreen mode Exit fullscreen mode
@Mutation() decorator

@Mutation is like the POST API of REST. It takes the first parameter as function, same as Query.
Below it, we define a function which can have any number of arguments. These variables are similar to body data in REST API. But we need to define these arguments through @Args decorator.

@Args() decorator

@Args is similar to @Field decorator that we learned earlier, except it takes first argument as name of argument and second as object which can define type and other things.

Lastly, inside the function, we call the service class function to add brand by passing the arguments we got through @Args.

We are almost done with Brands. Just a single step remaining, we need to all the Services and Resolver in the Brands module by:

import { Module } from '@nestjs/common';
import { BrandsService } from './brands.service';
import { BrandsResolver } from './brands.resolver';

@Module({
  providers: [BrandsService, BrandsResolver],
})
export class BrandsModule {}
Enter fullscreen mode Exit fullscreen mode
Brands API Testing

Now we have our Brands done. Let's test Brands API in playground. Open browser and go to http://localhost:3000/graphql
We can now see the Docs and Schema on the right side of playground.

Image description

And we can test any API, like:

Image description

Products

Now lets quickly create Products Module to handle Products entity.

Module

Generate module by
nest g module products

Model

Create a file inside /src/products:
products.model.ts:

import { Field, Int, ObjectType } from '@nestjs/graphql';

@ObjectType()
export class Products {
  @Field((type) => Int)
  id: number;

  @Field({ nullable: true })
  name?: string;

  @Field((type) => Int)
  brandId: number;
}
Enter fullscreen mode Exit fullscreen mode

We needed to add brandId as well to link the Brands with their Products, we are using one-many relationship here.

Service

Generate service by nest g service products.
products.service.ts:

import { Injectable } from '@nestjs/common';
import { Products } from './products.model';

@Injectable()
export class ProductsService {
  private readonly products: Products[] = [
    {
      id: 1,
      name: 'Product A',
      brandId: 1,
    },
    {
      id: 2,
      name: 'Product B',
      brandId: 1,
    },
    {
      id: 3,
      name: 'Product C',
      brandId: 2,
    },
  ];

  findById(id: number): Products {
    return this.products.find((product) => product.id == id);
  }

  findByBrandId(brandId: number): Products[] {
    return this.products.filter((product) => product.brandId == brandId);
  }

  addProduct(id: number, name: string, brandId: number): boolean {
    this.products.push({ id, name, brandId });
    return true;
  }
}
Enter fullscreen mode Exit fullscreen mode

Resolver

Make a file in /src/products:
products.resolver.ts:

import { Args, Int, Mutation, Query, Resolver } from '@nestjs/graphql';
import { Products } from './products.model';
import { ProductsService } from './products.service';

@Resolver(() => Products)
export class ProductsResolver {
  constructor(private productsService: ProductsService) {}
  @Query((returns) => [Products])
  getProductsByBrandId(
    @Args('brandId', { type: () => Int }) brandId: number,
  ): Products[] {
    return this.productsService.findByBrandId(brandId);
  }
  @Query((returns) => Products)
  getProductById(@Args('id', { type: () => Int }) id: number): Products {
    return this.productsService.findById(id);
  }
  @Mutation((returns) => Boolean)
  addProduct(
    @Args('id', { type: () => Int }) id: number,
    @Args('name') name: string,
    @Args('brandId', { type: () => Int }) brandId: number,
  ): boolean {
    return this.productsService.addProduct(id, name, brandId);
  }
}
Enter fullscreen mode Exit fullscreen mode

Modify the
products.module.ts:

import { Module } from '@nestjs/common';
import { ProductsService } from './products.service';
import { ProductsResolver } from './products.resolver';

@Module({
  providers: [ProductsService, ProductsResolver],
})
export class ProductsModule {}
Enter fullscreen mode Exit fullscreen mode

Linking Products to Brands

Here we need to modify some of the files of Brands Module. First of all, we need to pass ProductsService in providers of Brands so that we can use ProductsService to find the Products related to a Brand by using findByBrandId function that we defined in ProductsService class above.

brands.module.ts:

import { Module } from '@nestjs/common';
import { BrandsService } from './brands.service';
import { BrandsResolver } from './brands.resolver';
import { ProductsService } from 'src/products/products.service';

@Module({
  providers: [BrandsService, BrandsResolver, ProductsService],
})
export class BrandsModule {}
Enter fullscreen mode Exit fullscreen mode

We also need to add a field in Brands Model as products which will corresponds to Brands products
brands.model.ts:

import { Field, Int, ObjectType } from '@nestjs/graphql';
import { Products } from 'src/products/products.model';

@ObjectType()
export class Brands {
  @Field((type) => Int)
  id: number;

  @Field({ nullable: true })
  name?: string;

  @Field((type) => [Products], { defaultValue: [] })
  products?: Products[];
}
Enter fullscreen mode Exit fullscreen mode

Lastly, we need to Resolve this newly added field in Brands Resolver,
brands.resolver.ts:

import {
  Args,
  Int,
  Mutation,
  Parent,
  Query,
  ResolveField,
  Resolver,
} from '@nestjs/graphql';
import { Brands } from './brands.model';
import { BrandsService } from './brands.service';
import { Products } from 'src/products/products.model';
import { ProductsService } from 'src/products/products.service';

@Resolver(() => Brands)
export class BrandsResolver {
  constructor(
    private brandsService: BrandsService,
    private productsService: ProductsService,
  ) {}

  @ResolveField()
  products(@Parent() brand: Brands): Products[] {
    const brandId = brand.id;
    return this.productsService.findByBrandId(brandId);
  }

  @Query((returns) => [Brands])
  getAllBrands(): Brands[] {
    return this.brandsService.findAll();
  }

  @Query((returns) => Brands)
  getBrandById(@Args('id', { type: () => Int }) id: number): Brands {
    return this.brandsService.findById(id);
  }

  @Mutation((returns) => Brands)
  addBrand(
    @Args('id', { type: () => Int }) id: number,
    @Args('name') name: string,
  ): Brands {
    return this.brandsService.addBrand(id, name);
  }
}
Enter fullscreen mode Exit fullscreen mode
@ResolveField() decorator

We have added a @ResolveField decorator in the resolver file. When we have an object, Object A, as a field of other object, Object B, like Products as a field for Brands, we need to resolve that field, that is to define a way through which Object A is fetched on some link attribute. Here linking attribute is Brand ID. Brand ID is id in brands.model.ts and brandId in products.model.ts.

In the function provided after decorator, requires first argument as parent object, i.e Brands here. And we define it using @Parent decorator like:

...
...
@ResolveField()
  products(@Parent() brand: Brands)
...
...
Enter fullscreen mode Exit fullscreen mode

We can access Brands Id by brand.id and pass it to the ProductsService findByBrandId function.

That's it. Save everything and re run the server by:
nest start

Go to playground and verify that we can access Products of each Brands:

Image description


That's a wrap. Hope you found this article helpful.
You can access the repo here.
Keep learning ❤️

Top comments (0)