In this post I will show you how to implement NestJS😻 with GraphQL in schema first mode, using Mercurius and the "platform" to Fastify.
For those 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.
Mercurius, on the other hand, is a GraphQL adapter for Fastify (for others information see the documentation here).
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
Then create a NestJS project.
$ nest new nestjs-graphql-mercurius-schema-first
$ cd nestjs-graphql-mercurius-schema-first
// start the application
$ npm run start:dev
Open the browser on localhost:3000
to verify that hello world is displayed.
Well, the first thing to do before installing the dependencies for graphql
and mercurius
is to install the @nestjs/platform-fastify
package instead of the default express-based @nestjs/platform-express
package, because mercurius
works with Fastify
and not with Express
, to do so follow these steps:
npm remove @nestjs/platform-express
and install the dependency for Fastify
npm install @nestjs/platform-fastify
We now modify the file main.ts
, configuring the adapter for Fastify
within the boostrap function, like this:
import { NestFactory } from '@nestjs/core';
import {
FastifyAdapter,
NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter()
);
await app.listen(3000);
}
bootstrap();
now we open the browser again on localhost:3000
or via curl (curl -H 'content-type: application/json' -v -X GET http://127.0.0.1:3000
) to verify that "hello world" is displayed.
Good! Now let's proceed with configuring the database with docker by creating a docker-compose.yml
file to create the PostgreSQL
service, things follows:
services:
db:
image: postgres
restart: always
ports:
- "5432:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: pass123
POSTGRES_DB: postgres
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/mercurius graphql mercurius
Create a GraphqlOptions
class for GraphQL settings, as follows:
import { GqlOptionsFactory } from '@nestjs/graphql';
import { Injectable } from '@nestjs/common';
import { join } from 'path';
import { MercuriusDriverConfig } from '@nestjs/mercurius';
@Injectable()
export class GraphqlOptions implements GqlOptionsFactory {
createGqlOptions(): Promise<MercuriusDriverConfig> | MercuriusDriverConfig {
return {
subscription: true,
graphiql: true,
typePaths: ['./**/*.graphql'],
definitions: {
path: join('./graphql.schema.ts'),
outputAs: 'class',
},
};
}
}
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({
imports:[
GraphQLModule.forRootAsync<MercuriusDriverConfig>({
driver: MercuriusDriver,
useClass: GraphqlOptions,
}),
],
})
export class AppModule {}
Now let's create a file called users.graphql
, where we are going to write our scheme that allows us to interact later with Graphiql 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
}
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
We also install another package for configuration files such as .env
files (environments)
$ npm install --save @nestjs/config
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 {}
Create a .env file, as follows:
DATABASE_HOST=localhost
DATABASE_USER=postgres
DATABASE_PASSWORD=pass123
DATABASE_NAME=postgres
DATABASE_PORT=5432
If you have trouble setting up TypeOrmModule
here, 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 { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
username: string;
@Column({
unique: true,
})
email: string;
@Column({ length: 60 })
password: string;
}
Create Data Transfer Objects (Dto) class to create the user, which we will call CreateUserInput
:
import { Field, InputType } from '@nestjs/graphql';
import { MaxLength, IsNotEmpty, IsEmail, IsString } from 'class-validator';
@InputType()
export class CreateUserInput {
@IsString()
@MaxLength(30)
@Field()
readonly name: string;
@IsString()
@MaxLength(40)
@Field()
readonly username: string;
@IsEmail()
@Field()
readonly email: string;
@IsNotEmpty()
@IsString()
@MaxLength(60)
@Field()
password: string;
}
Remember to install this package before creating the dto class for the upgrade.
$ npm i @nestjs/mapped-types
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) {}
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();
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
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;
}
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 { ErrorWithProps } from 'mercurius';
@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 ErrorWithProps(`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 ErrorWithProps(`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);
}
}
UserResolver:
import { NotFoundException } from '@nestjs/common';
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 { ErrorWithProps } from 'mercurius';
@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 ErrorWithProps(`User #${id} not found`);
}
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);
}
}
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 {}
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 schema.gql
file is generated, once done, from the browser we go to the address http://localhost:3000/graphiql
thus starting our GraphiQL
( the Graphiql Playground
for Mercurius
), a UI that allows us to interact with our database.
I add below the Queries and Mutations to run in the Graphiql:
getUsers
{
users {
id
name
email
username
password
}
}
getUserById
{
user(id: "1") {
id
name
email
username
password
}
}
addUser
mutation {
createUser(createUserInput: {
name: "tony"
email:"tony_admin@nest.it"
username: "tony_admin"
password: "secret123"
}) {
name
email
username
password
}
}
updateUser
mutation {
updateUser(
updateUserInput: {
name: "tony"
email: "tony_admin@nest.it"
username: "tony_admin"
password: "secret123"
}
id: "1"
) {
name
email
username
password
}
}
removeUser
mutation {
removeUser(id: "3") {
name
email
username
password
}
}
That's all.
I hope it will be helpful for anything write me in the comments as well.😉
Top comments (0)