DEV Community

Cover image for How YOU can use Nest to build a GraphQL API
Chris Noring for Microsoft Azure

Posted on • Edited on

How YOU can use Nest to build a GraphQL API

Follow me on Twitter, happy to take your suggestions on topics or improvements /Chris

There are many ways to build a GraphQL API. Looking at JavaScript you might be using the raw grapql library, the graphql-express library or why not the one from apollo. There is another really great way and that is Nest.js that nicely integrates with GraphQL

In this article we will:

  • Explain GraphQL fundamentals quickly. We will explain enough for you to understand the major constructs.
  • Create a first Nest.js + GraphQL project and see what a full CRUD looks like
  • Best practices let's see what we can do to leverage the full power of Nest

 GraphQL fundamentals

I've explained the fundamentals of Graphql in the following articles:

This article would be crazy long if we added a full primer to GraphQL so let's be happy by stating a GraphQL API consist of a schema and resolver functions.

Create your first Hello GraphQL in Nest.js

Ok, now we have a basic understanding of how GraphQL works. It's time to do the following:

  1. Scaffold a Nest project
  2. Wire up the project to use GraphQL
  3. Write our schema and resolvers

 Scaffold a Nest.js project

To scaffold a new project just type the following:

nest new hello-world
Enter fullscreen mode Exit fullscreen mode

You can replace hello-world with the name of your project. This will give you the needed files for our next step, which is to add GraphQL.

Wire up GraphQL

Now to use GraphQL in the project we just created we need to do the following:

  1. install the needed dependencies
  2. Configure the GraphQLModule

Ok, to install the dependencies we need to type:

npm i --save @nestjs/graphql apollo-server-express graphql
Enter fullscreen mode Exit fullscreen mode

The above will give us the needed GraphQL binding for Nest @nestjs/graphql and the Apollo library for GraphQL server creation apollo-server-express.

Next up we need to configure something called GraphQLModule that we get from the library @nestjs/graphql. There are many ways to set this up but what we will tell it, at this point, is where to find the schema file. Therefore we will change app.module.ts to look like the following:

// app.module.ts

import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { AppResolver } from './app.resolver';
// import { join } from 'path';

@Module({
  imports: [
    GraphQLModule.forRoot({
      debug: false,
      playground: true,
      typePaths: ['./**/*.graphql']
    }),
  ],
  providers: [ AppResolver ]
})
export class AppModule { }
Enter fullscreen mode Exit fullscreen mode

Let's have a closer look at the GraphQLModule.forRoot() invocation. Now, we see here that we set playground to true. This will give us a graphical way to pose our queries, more on that later. We also see that we set a property called typePaths and give it an array looking like so ['./**/*.graphql']. Now, this is a pattern matching looking for all files ending with .graphql. The reason for this construct is that we can actually spread out our schema definition on several files.

Write our schema and resolvers

Next step is to create a file matching the above pattern so we create a file called app.graphql and we give it the following content:

// app.graphql

type Cat {
  id: Int
  name: String
  age: Int
}

type Query {
  getCats: [Cat]
  cat(id: ID!): Cat
}
Enter fullscreen mode Exit fullscreen mode

Now this sets us up nicely, but what about resolver functions? Well, lets head back to app.module.ts and zoom in on a specific row providers: [ AppResolver ]. This is us wiring up AppResolver that will act as our resolver class. Let's have a closer look at AppResolver:

// app.resolver.ts

import { Args, Mutation, Query, Resolver, Subscription } from '@nestjs/graphql';
import { ParseIntPipe } from '@nestjs/common';


@Resolver('Cat')
export class AppResolver {
  cats = [{
    id: 1,
    name: 'Mjau',
    age: 17
  }]

  @Query()
  getCats() {
    console.log('getCats');
    return this.cats;
  }

  @Query('cat')
  async findOneById(
    @Args('id', ParseIntPipe)
    id: number,
  ): Promise<any> {
    return this.cats.find(c => c.id === id);
  }

}
Enter fullscreen mode Exit fullscreen mode

As you can see we create a class AppResolver but it also comes with some interesting decorators. Let's explain those:

  • @Resolver, this decorator tells GraphQL that this class should know how to resolve anything related to type Cat.
  • Query(), this says that the method being decorated by this will namewise match something defined in the Query in the schema. As we can see we have the method getCats() but in the case we don't plan to name match we need to send an arg into Query that says what part it matches. As you can see on the method findOneById() we decorate it with Query('cat') which simply means it resolves any queries to cat
  • @Args, this decorator is used as a helper decorator to dig out any input parameters

Take it for a spin

Let's first ensure we have all the necessary libraries by first typing:

npm install
Enter fullscreen mode Exit fullscreen mode

This will install all needed dependencies. Once this finishes we should be ready to start.

Next type so we can try out our API:

npm start
Enter fullscreen mode Exit fullscreen mode

It should look something like this:

Next step is to go to our browser at http://localhost:3000/graphql. You should see the following:

As you can see by above image we have defined two different queries called oneCat and allCats and you can see the query definition in each. In the one called oneCat you can see how we call { cat(id: 1){ name } } which means we invoke the resolver for cat with the parameter id and value 1 and we select the field name on the result, which is of type Cat. The other query allCats are simple calling { getCats } which matches with the same method on the AppResolver class

 Adding mutators

So far we have a fully working GraphQL API that works to query against but we are missing the mutator part, what if we want to support adding a cat, updating it or deleting it? To do that we need to do the following:

  1. Add mutator operations to our schema
  2. Add the needed resolver methods to our AppResolver class
  3. Test it out

Updating our schema

Ok, we need to add some mutators to the schema, ensure app.graphql now looks like the following:

type Cat {
  id: Int
  name: String
  age: Int
}

input CatInput {
  name: String
  age: Int,
  id: Int
}

type Mutation {
  createCat(cat: CatInput): String,
  updateCat(cat: CatInput): String,
  deleteCat(id: ID!): String
}

type Query {
  getCats: [Cat]
  cat(id: ID!): Cat
}
Enter fullscreen mode Exit fullscreen mode

As you can see above we've added Mutation and CatInput

Add resolvers

Ok, now we need to head back to AppResolver class and ensure it now looks like this:

// app.resolver.ts

import { Args, Mutation, Query, Resolver, Subscription } from '@nestjs/graphql';
import { ParseIntPipe } from '@nestjs/common';



@Resolver('Cat')
export class AppResolver {
  cats = [{
    id: 1,
    name: 'Cat1',
    age: 17
  }]

  @Mutation()
  createCat(
    @Args('cat')
    cat: any
  ): Promise<string> {
    this.cats = [...this.cats, {...cat, id: this.cats.length + 1}];
    return Promise.resolve('cat created');
  }

  @Mutation()
  updateCat(
    @Args('cat')
    cat: any
  ): Promise<string> {
    this.cats = this.cats.map(c => {
      if(c.id === cat.id) {
        return {...cat}
      }
      return c;
    });
    return Promise.resolve('cat updated');
  }

  @Mutation()
  deleteCat(
    @Args('id', ParseIntPipe)
    id: number
  ) : Promise<any> {
    this.cats = this.cats.filter(c => c.id !== id);
    return Promise.resolve('cat removed');
  }


  @Query()
  getCats() {
    console.log('getCats');
    return this.cats;
  }

  @Query('cat')
  async findOneById(
    @Args('id', ParseIntPipe)
    id: number,
  ): Promise<any> {
    return this.cats.find(c => c.id === id);
  }

}
Enter fullscreen mode Exit fullscreen mode

The added parts are the methods deleteCat(), updateCat() and createCat().

Additional features

We have a fully functioning API at this point. In fact, ensure your browser window looks like this and you will be able to test the full CRUD:

What do we mean by best practices? Well, we can do more than this to make our API easier to use like:

  1. Add types, right now we have defined a lot of types in our app.graphql file but we could extract those types and use them in the resolver class
  2. Split up our API, there is no need to have one gigantic schema file you can definitely split this up and let Nest stitch up all those files
  3. Define the API by decorating DTOs, there is a second way of defining an API, what way is the best is up to you to judge

Add types

I said we could extract the types from our schema to use them in the resolver class. That sounds great but I guess you are wondering how?

Well, you first need to head to app.module.ts and a property definitions and tell it two things. The first is what to name the file of generated types and secondly is what output type. The latter has two choices, class or interface. Your file should now look like this:

@Module({
  imports: [
    GraphQLModule.forRoot({
      debug: false,
      playground: true,
      typePaths: ['./**/*.graphql'],
      definitions: {
        path: join(process.cwd(), 'src/graphql.ts'),
        outputAs: 'class',
      }
    }),
  ],
  providers: [ AppResolver ]
})
export class AppModule { }
Enter fullscreen mode Exit fullscreen mode

If you start up the API with npm start then src/graphql.ts will be created and it should look like this:

//graphql.ts


/** ------------------------------------------------------
 * THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
 * -------------------------------------------------------
 */

/* tslint:disable */
export class CatInput {
    name?: string;
    age?: number;
    id?: number;
}

export class Cat {
    id?: number;
    name?: string;
    age?: number;
}

export abstract class IMutation {
    abstract createCat(cat?: CatInput): string | Promise<string>;

    abstract updateCat(cat?: CatInput): string | Promise<string>;

    abstract deleteCat(id: string): string | Promise<string>;
}

export abstract class IQuery {
    abstract getCats(): Cat[] | Promise<Cat[]>;

    abstract cat(id: string): Cat | Promise<Cat>;
}

Enter fullscreen mode Exit fullscreen mode

The takeaway for us is the types Cat and CatInput which we can use to make our AppResolver class a bit more type safe. Your app.resolver.ts file should now look like this:

// app.resolver.ts

import { Args, Mutation, Query, Resolver, Subscription } from '@nestjs/graphql';
import { ParseIntPipe } from '@nestjs/common';
import { Cat, CatInput } from './graphql';



@Resolver('Cat')
export class AppResolver {
  cats:Array<Cat> = [{
    id: 1,
    name: 'Cat1',
    age: 17
  }]

  @Mutation()
  createCat(
    @Args('cat')
    cat: CatInput
  ): Promise<string> {
    this.cats = [...this.cats, {...cat, id: this.cats.length + 1}];
    return Promise.resolve('cat created');
  }

  @Mutation()
  updateCat(
    @Args('cat')
    cat: CatInput
  ): Promise<string> {
    this.cats = this.cats.map(c => {
      if(c.id === cat.id) {
        return {...cat}
      }
      return c;
    });
    return Promise.resolve('cat updated');
  }

  @Mutation()
  deleteCat(
    @Args('id', ParseIntPipe)
    id: number
  ) : Promise<any> {
    this.cats = this.cats.filter(c => c.id !== id);
    return Promise.resolve('cat removed');
  }

  @Query()
  getCats(): Array<Cat> {
    return this.cats;
  }

  @Query('cat')
  async findOneById(
    @Args('id', ParseIntPipe)
    id: number,
  ): Promise<Cat> {
    return this.cats.find(c => c.id === id);
  }

}
Enter fullscreen mode Exit fullscreen mode

Worth noting above is how our internal array cats is now of type Cat and the methods createCat() and updateCat() now has input of type CatInput. Furthermore the method getCats() return an array of Cat and lastly how the method findOneById() return a Promise of type Cat.

Split up our schema definitions

Now we said we could easily do this because of the way things are set up. This is easy to do just by creating another file called **.graphql. So when should I do that? Well, when you have different topics in your API it makes sense to do the split. Let's say you were adding dogs then it would make sense to have a separate dogs.graphql and also a separate resolver class for dogs.

The point of this article was to show you how you could start and how you should gradually continue to add new types and new resolvers. I hope you found it useful.

 2nd way of defining things

The second way of defining a schema is outside the scope of this article, cause it would just be too long. However, have a look at how this is done is this repo and have a read here under the headline "Code First"

Summary

Now we have gone all the way from generating a new project, learned to define a schema and it's resolvers to generating types from our schema. We should be really proud of ourselves.

Top comments (5)

Collapse
 
owen profile image
owen

Hello, very interesting ! By the way, I've made a similar framework but you can also define types using decorators and more... (I wrote an article if you are interested => dev.to/owen/rakkit-create-your-gra...)

Collapse
 
felzan profile image
Luís Felipe Zanatto Alves

Very good!
I followed this and is working just fine! I'm going to create a BFF (calling REST APIs) with this pattern and i fill a lacking of best practices to organize the Schemas and the http calls. Do you have some recommendation?

Collapse
 
softchris profile image
Chris Noring

the recommended practice is to have one schema per feature, so one for products, one for users etc. Any schema ending in .graphql will be automatically found and merged into one schema. Hope that's what you were looking for :)

Collapse
 
pulakb profile image
Pulak

Just noticed that one should run npm install before npm start

Collapse
 
softchris profile image
Chris Noring

Thanks, updated the text