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:
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.
Multiple endpoints: Usually REST Api has multiple points, one for each type of data structure.
GraphQL solves both of these problems by:
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.
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
First, we need to have nestjs/cli installed. Here is the command that will install the nestjs/cli:
npm i -g @nestjs/cli
Once, we have installed nestjs cli, we can create a nest project by
nest new <project-name>
.Then it will ask you the package manager that you want to use for this project, as shown below:
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:
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.
- 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 {}
The configuration of GrapqhQL, which is provided as an object in forRoot:
{
driver: ApolloDriver,
autoSchemaFile: true,
}
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:
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
}
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;
}
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;
}
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 };
}
}
@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);
}
}
@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,
) {}
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();
}
@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);
}
@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 {}
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.
And we can test any API, like:
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;
}
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;
}
}
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);
}
}
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 {}
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 {}
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[];
}
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);
}
}
@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)
...
...
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:
That's a wrap. Hope you found this article helpful.
You can access the repo here.
Keep learning ❤️
Top comments (0)