DEV Community

Antonio Tripodi
Antonio Tripodi

Posted on • Edited on

Simple example GraphQL(Schema First) with NestJS 9.x/10.x 😻

In this post I will show you how to implement NestJS😻 with GraphQL, in schema first mode. For those who would like to use the code first mode I made a post here

For those unfamiliar with or unfamiliar with NestJS, it's a Node.js TypeScript framework that helps you build efficient and scalable enterprise-grade Node.js applications.

While for those unfamiliar with GraphQL it is a powerful query language for APIs and a runtime to satisfy those queries with existing data. It's an elegant approach that solves many typical REST API problems.

I attach here the link to see the difference between GraphQL vs REST.

The ways to use GraphQL are two first the code or first the schema, the first from the code we create the schema for GraphQL while the second we create our schema that interfaces with GraphQL

In this example we are going to see the schema first mode with the use of TypeORM, but you can use whatever you like.

Well now after this short intro let's get started!

So let's get started by creating the NestJS app

Open Terminal and install CLI for NestJS, if you have already installed it, skip this step.

$ npm i -g @nestjs/cli
Enter fullscreen mode Exit fullscreen mode

Then create a NestJS project.

$ nest new nestjs-graphql-schema-first
$ cd nestjs-graphql-schema-first
// start the application
$ npm run start:dev
Enter fullscreen mode Exit fullscreen mode

Open the browser on localhost:3000 to verify that hello world is displayed.

then we create a docker-compose.yml file for create service PostgreSQL

version: "3"
services:
  db:
    image: postgres
    restart: always
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: pass123
      POSTGRES_DB: postgres

Enter fullscreen mode Exit fullscreen mode

for those who do not know what docker is I leave you the link here for more information: https://www.docker.com/get-started

There are many different ways to integrate Nest with databases, and all of them depend on personal preferences or project needs.

Install graphql dependencies and devDependencies

$ npm i @nestjs/graphql @nestjs/apollo @apollo/server graphql
Enter fullscreen mode Exit fullscreen mode

Create a GraphqlOptions class for GraphQL settings, as follows:

import { GqlOptionsFactory } from '@nestjs/graphql';
import { Injectable } from '@nestjs/common';
import { join } from 'path';
import { ApolloDriverConfig } from '@nestjs/apollo';

@Injectable()
export class GraphqlOptions implements GqlOptionsFactory {
  createGqlOptions(): Promise<ApolloDriverConfig> | ApolloDriverConfig {
    return {
      installSubscriptionHandlers: true,
      debug: true,
      playground: true,
      typePaths: ['./**/*.graphql'],
      definitions: {
        path: join(process.cwd(), 'src/graphql.schema.ts'),
        outputAs: 'class',
      },
    };
  }
}

Enter fullscreen mode Exit fullscreen mode

An important thing about this configuration is to set where we want the graphql.schema.ts file to be generated, you can call it whatever you want, the important thing is to prefix it with* .ts.

Well now let's move on to registering GraphQLModule in AppModule:

@Module({
    GraphQLModule.forRootAsync<ApolloDriverConfig>({
      driver: ApolloDriver,
      useClass: GraphqlOptions,
    }),
})
export class AppModule {}

Enter fullscreen mode Exit fullscreen mode

Now let's create a file called users.graphql, where we are going to write our scheme that allows us to interact later with Graphql Playground:

type User {
  id: ID
  name: String!
  email: String!
  username: String!
  password: String!
}

input CreateUserInput {
  name: String!
  email: String!
  username: String!
  password: String!
}

input UpdateUserInput {
  name: String!
  email: String!
  username: String!
  password: String!
}

type Query {
  users: [User]!
  user(id: Int!): User
}

type Mutation {
  createUser(createUserInput: CreateUserInput!): User!
  updateUser(id: Int!, updateUserInput: UpdateUserInput!): User!
  removeUser(id: Int!): User
}

Enter fullscreen mode Exit fullscreen mode

In this file we have set up a lot of things before, we have a User object type and two inputs which are like DTO, which in turn allows us to define how the request should look. To complete our users.graphql file we have created two other types called Query and Mutation.
In very simple words, the use of the query in graphql is used to retrieve the data while the mutation is used for operations such as inserting and updating data or to delete data.

Well, now let's move on to installing dependencies for Typeorm.

Install TypeORM and PostgreSQL dependencies

$ npm install --save @nestjs/typeorm typeorm pg
Enter fullscreen mode Exit fullscreen mode

We also install another package for configuration files such as .env files (environments)

$ npm install --save @nestjs/config
Enter fullscreen mode Exit fullscreen mode

Set TypeOrmModule and ConfigModule in AppModule

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module ({
   imports: [
    ConfigModule.forRoot(),
    TypeOrmModule.forRootAsync({
      useFactory: () => ({
        type: 'postgres',
        host: process.env.DATABASE_HOST,
        port: +process.env.DATABASE_PORT,
        username: process.env.DATABASE_USER,
        password: process.env.DATABASE_PASSWORD,
        database: process.env.DATABASE_NAME,
        autoLoadEntities: true,
        synchronize: true,
      }),
    }),
   ],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Create a .env file, as follows:

  DATABASE_HOST=localhost
  DATABASE_USER=postgres
  DATABASE_PASSWORD=pass123
  DATABASE_NAME=postgres
  DATABASE_PORT=5432
Enter fullscreen mode Exit fullscreen mode

If you have trouble setting up TypeOrmModule, make sure Docker is running with docker compose up.
Also make sure the database name inside your .env matches the one you have in your docker-compose file.

Well now let's create our entity we call it users.entity.ts:

import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
} from 'typeorm';
import { Field, ID, ObjectType } from '@nestjs/graphql';

@Entity()
@ObjectType()
export class Users {
  @Field(() => ID)
  @PrimaryGeneratedColumn()
  id: number;

  @Field()
  @Column()
  name: string;

  @Field()
  @Column()
  username: string;

  @Field()
  @Column()
  email: string;

  @Field()
  @Column({ length: 60 })
  password: string;
}
Enter fullscreen mode Exit fullscreen mode

Create Data Transfer Objects (Dto) class to create the user, which we will call CreateUserInput:

import { Field, InputType } from '@nestjs/graphql';
import { IsEmail, IsNotEmpty, IsString, MaxLength } from 'class-validator';

@InputType()
export class CreateUserInput {
  @Field()
  @IsString()
  readonly name: string;

  @Field()
  @IsEmail()
  readonly email: string;

  @Field()
  @IsString()
  @MaxLength(40)
  readonly username: string;

  @Field()
  @IsNotEmpty()
  @IsString()
  @MaxLength(60)
  password: string;
}

Enter fullscreen mode Exit fullscreen mode

Remember to install this package before creating the dto class for the upgrade.

$ npm i @nestjs/mapped-types
Enter fullscreen mode Exit fullscreen mode

Well, now to update the user data we extend the CreateUserInput class:

import { InputType, PartialType } from '@nestjs/graphql';
import { CreateUserInput } from './create-user.input';

@InputType()
export class UpdateUserInput extends PartialType(CreateUserInput) {}
Enter fullscreen mode Exit fullscreen mode

we call validation pipe in the main.ts file as follows:

import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      transform: true,
      forbidNonWhitelisted: true,
      transformOptions: {
        enableImplicitConversion: true,
      },
    }),
  );
  await app.listen(3000);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

Well, now we're going to create a simple service, resolver, and user module

$ nest g module users
$ nest g service users
$ nest g resolver users
Enter fullscreen mode Exit fullscreen mode

You should now have a users folder with UsersModule, UsersService, and UsersResolver inside.

Now before we write our service, let's create a final dto file for the pagination, as follows:

import { ArgsType, Field, Int } from '@nestjs/graphql';
import { Max, Min } from 'class-validator';

@ArgsType()
export class UsersArgs {
  @Field(() => Int)
  @Min(0)
  offset = 0;

  @Field(() => Int)
  @Min(1)
  @Max(50)
  limit = 25;
}
Enter fullscreen mode Exit fullscreen mode

Now let's start writing our UsersService:

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import * as bcrypt from 'bcrypt';
import { Repository } from 'typeorm';
import { CreateUserInput } from './dto/create-user.input';
import { UpdateUserInput } from './dto/update-user.input';
import { UsersArgs } from './dto/users.args';
import { Users } from './entities/users.entity';
import { UserInputError } from '@nestjs/apollo';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(Users)
    private readonly usersRepository: Repository<Users>,
  ) {}

  public async findAll(usersArgs: UsersArgs): Promise<Users[]> 
  {
    const { limit, offset } = usersArgs;
    return this.usersRepository.find({
      skip: offset,
      take: limit,
    });
  }

  public async findOneById(id: string): Promise<Users> {
    const user = await this.usersRepository.findOne(id);

    if (!user) {
      throw new UserInputError(`User #${id} not found`);
    }
    return user;
  }

  public async create(createUserInput: CreateUserInput): Promise<Users> {
    createUserInput.password = bcrypt.hashSync(createUserInput.password, 8);

    const user = this.usersRepository.create({ ...createUserInput});
    return this.usersRepository.save(user);
  }

  public async update(
    id: string,
    updateUserInput: UpdateUserInput,
  ): Promise<Users> {
    updateUserInput.password = bcrypt.hashSync(updateUserInput.password, 8);

    const user = await this.usersRepository.preload({
      id: +id,
      ...updateUserInput,
    });

    if (!user) {
      throw new UserInputError(`User #${id} not found`);
    }
    return this.usersRepository.save(user);
  }

  public async remove(id: string): Promise<any> {
    const user = await this.findOneById(id);
    return this.usersRepository.remove(user);
  }
}
Enter fullscreen mode Exit fullscreen mode

UserResolver:

import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { CreateUserInput, UpdateUserInput, UsersArgs } from './dto';
import { UsersService } from './users.service';
import { Users } from './entities/users.entity';
import { UserInputError } from '@nestjs/apollo';

@Resolver()
export class UsersResolver {
  constructor(private readonly usersService: UsersService) {}

  @Query(() => [Users])
  public async users(@Args() usersArgs: UsersArgs): Promise<Users[]> {
    return this.usersService.findAll(usersArgs);
  }

  @Query(() => Users)
  public async user(@Args('id') id: string): Promise<Users> {
    const user = await this.usersService.findOneById(id);
    if (!user) {
      throw new UserInputError(id);
    }
    return user;
  }

  @Mutation(() => Users)
  public async createUser(
    @Args('createUserInput') createUserInput: CreateUserInput,
  ): Promise<Users> {
    return await this.usersService.create(createUserInput);
  }

  @Mutation(() => Users)
  public async updateUser(
    @Args('id') id: string,
    @Args('updateUserInput') updateUserInput: UpdateUserInput,
  ): Promise<Users> {
    return await this.usersService.update(id, updateUserInput);
  }

  @Mutation(() => Users)
  public async removeUser(@Args('id') id: string): Promise<any> {
    return this.usersService.remove(id);
  }
}
Enter fullscreen mode Exit fullscreen mode

Now register in our UsersModule our service and the resolver, like this:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersService } from './users.service';
import { UsersResolver } from './users.resolver';
import { Users } from './entities/users.entity';

@Module({
  imports: [TypeOrmModule.forFeature([Users])],
  providers: [UsersService, UsersResolver],
})
export class UsersModule {}
Enter fullscreen mode Exit fullscreen mode

Well now let's try to interact with Graphql to see if everything works, but before we have to verify that starting our application the graphql.schema.ts file is generated, once done, from the browser we go to the address http://localhost:3000/graphql thus starting our GraphQL Playground, a UI that allows us to interact with our database.
Remember, inside the generated file graphql.schema.ts, we have the compilation of everything we have set inside the users.graphql and all the * .graphql files we create inside the modules, such as post.graphql or roles.graphql, are added in cascade inside our graphql.schema.ts file

If we want to update our updated graphql.schema.ts file with the new changes I make, without going to delete the file, we create a new file and call it generate-typings.ts, now we create a factory class to set the generator as follows:

import { GraphQLDefinitionsFactory } from '@nestjs/graphql';
import { join } from 'path';

const definitionsFactory = new GraphQLDefinitionsFactory();
definitionsFactory.generate({
  typePaths: ['./src/**/*.graphql'],
  path: join(process.cwd(), 'src/graphql.schema.ts'),
  outputAs: 'class',
});
Enter fullscreen mode Exit fullscreen mode

now from the terminal we run the following command:

$ ts-node  generate-typings
Enter fullscreen mode Exit fullscreen mode

I add below the Queries and Mutations to run in the GraphQL Playground:

getUsers

{
    users {
        id
        name
        email
        username
        password
    }
}

Enter fullscreen mode Exit fullscreen mode

getUserById

{
    user(id: "1") {
        id
        name
        email
        username
        password
    }
}

Enter fullscreen mode Exit fullscreen mode

addUser


mutation {
    createUser(createUserInput: {
        name: "tony"
        email:"tony_admin@nest.com"
        username: "tony_admin"
        password: "secret123"
    }) {
        name
        email
        username
        password
    }
}

Enter fullscreen mode Exit fullscreen mode

updateUser

mutation {
    updateUser(
        updateUserInput: {
            name: "tony"
            email: "tony_admin@nest.com"
            username: "tony_admin"
            password: "secret123"
        }
        id: "1"
    ) {
        name
        email
        username
        password
    }
}
Enter fullscreen mode Exit fullscreen mode

removeUser

mutation {
    removeUser(id: "3") {
        name
        email
        username
        password
    }
}
Enter fullscreen mode Exit fullscreen mode

Now I leave it to you to create a post module and a role module for users.

That's all.

I hope it will be helpful for anything write me in the comments as well.😉

Top comments (2)

Collapse
 
reharik profile image
Raif Harik

Hi, this seems reasonable. If I read it correctly it seems we are using the resolver essentially as a "service" to retrieve ( from the repository ) the data, structure it and return it.

My first question would be what happens when your domain model does not exactly match you data tables ( as it shouldn't since they are for two wildly different things )? Is there any support in typeorm for the actual M part? The mapping of relational data structure to object data structure?
Another question is, does this setup handle the n+1 query problem inherent in gql? I.e. user has a collection of dogs. Each do has a collection of fleas. I need the names of all the fleas. This creates a great number of DB queries. N+1 queries in fact :).
If you know of someone docs or articles that address these questions that would be great. Or if you want to write a post on it even better.
Thanks
Raif

Collapse
 
tony133 profile image
Antonio Tripodi • Edited

Hi,

For the first question, if I understand correctly, you are looking for documentation on how to create relationships between tables with Typeorm, right? you can see in the official documentation, see here: typeorm.io/relations
I personally don't have a favorite ORM, in this tutorial I used TypeORM because I saw that it is the most used with NestJS and so I used that :-), but you can use whatever you prefer :-)  

While for the second question regarding the GraphQL 1+n problem in this example is not implemented, I was planning to make a separate post or update this one, just due to lack of time I could not do it :-)
To get an example you can take a look at this link: wanago.io/2021/02/08/api-nestjs-n-..., you can find many more examples about NestJS ( relations, etc..)

i hope i was helpful