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, thegraphql-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:
- Scaffold a Nest project
- Wire up the project to use GraphQL
- Write our schema and resolvers
Scaffold a Nest.js project
To scaffold a new project just type the following:
nest new hello-world
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:
- install the needed dependencies
- Configure the
GraphQLModule
Ok, to install the dependencies we need to type:
npm i --save @nestjs/graphql apollo-server-express graphql
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 { }
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
}
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);
}
}
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 typeCat
. -
Query()
, this says that the method being decorated by this will namewise match something defined in theQuery
in the schema. As we can see we have the methodgetCats()
but in the case we don't plan to name match we need to send an arg intoQuery
that says what part it matches. As you can see on the methodfindOneById()
we decorate it withQuery('cat')
which simply means it resolves any queries tocat
-
@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
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
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:
- Add mutator operations to our schema
- Add the needed resolver methods to our
AppResolver
class - 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
}
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);
}
}
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:
-
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 - 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
- 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 { }
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>;
}
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);
}
}
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)
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...)
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?
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 :)
Just noticed that one should run npm install before npm start
Thanks, updated the text