GraphQL + NodeJS + Postgres — Made easy with NestJS and TypeORM
In this article I will show you how to add a TypeORM module into our GraphQL backend project in a very, very, very easy way… using NestJS and TypeORM.
Github : https://github.com/tkssharma/blogs/tree/master/nestjs-with-graphql-code-first
NestJS comes with an easy to use TypeORM module and in this article we are going to connect to a local Postgres database. Why Postgres? Because usually enterprises uses Postgres over Mysql even though TypeORM can work with any relational databases and … even Mongo (I still prefer mongoose though)
If you are lazy enough, in the end of article you can find all the source code in a public git repository.
Quick check-up:
- I’m using node v14.16.1 when writing this article (I know… I need to update it)
- Nest CLI version is: 8.1.1
- npm version: 7.12.1
- yarn version: 1.22.10 (I still like yarn over npm)
Everything starts in the beginning and for us we are going to configure our local Postgres. For that I am going to use Docker and docker compose. My current docker version when I am writing is the following:
Docker version 20.10.6, build 370c289
This version already contains docker compose commands by default so I won’t be expecting anything to be added here.
In the root of our project just add this simple file, named docker-compose.yml:
version: "3"
services:
db:
image: postgres
restart: unless-stopped
ports:
- "5432:5432"
environment:
POSTGRES_PASSWORD: SuperSecret!23
volumes:
- .docker-data/postgres:/var/lib/postgresql/data
To start your local database just run in your terminal the following:
docker compose up -d
where “docker compose” is the command that will read the docker-compose.yml configuration, “up” will start the services (db -> Postgres) and “-d” will detach from your terminal to run in the background. If you want to stop it, just run docker compose down.
To make sure it is running, you can try: docker ps
nestjs-with-graphql git:(002)
docker-compose up -d
Docker Compose is now in the Docker CLI, try `docker compose up`Creating network "nestjs-with-graphql_default" with the default driver
Creating nestjs-with-graphql_db_1 ... done
➜ nestjs-with-graphql git:(002) ✗ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
9fa6411a763e postgres "docker-entrypoint.s…" 12 seconds ago Up 9 seconds 0.0.0.0:5432->5432/tcp, :::5432->5432/tcp nestjs-with-graphql_db_1
That’s it. We are not go deeper here since the focus is the NodeJS and Postgres but if we don’t have a local database, there’s nothing much to do here…
To install the modules on NestJS, let’s run the following:
yarn add typeorm pg @nestjs/typeorm
With this we are adding the typeorm library, the NestJS integration (@nestjs/typeorm) and the Postgres module (pg) (with MySQL you should change to something like mysql2)
Great, now for the basics:
In our app.module.ts, let’s add the TypeORM module right after the GraphQLModule:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { GraphQLModule } from '@nestjs/graphql';
import { UsersModule } from './users/users.module';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
GraphQLModule._forRoot_({
autoSchemaFile: './schema.gql',
debug: true,
playground: true,
}),
TypeOrmModule._forRoot_({
keepConnectionAlive: true,
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'postgres',
password: 'SuperSecret!23',
database: 'postgres',
autoLoadEntities: true,
synchronize: true,
}),
UsersModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
We are going to use .env vars over hardcoded later on. Let’s now just try to run our project to see if there’s no error:
nestjs-with-graphql git:(002) ✗ yarn start
yarn run v1.22.10
warning ../../package.json: No license field
$ nest start
[Nest] 9994 - 08/09/2021, 9:42:11 PM LOG [NestFactory] Starting Nest application...
[Nest] 9994 - 08/09/2021, 9:42:12 PM LOG [InstanceLoader] TypeOrmModule dependencies initialized +53ms
[Nest] 9994 - 08/09/2021, 9:42:12 PM LOG [InstanceLoader] UsersModule dependencies initialized +1ms
[Nest] 9994 - 08/09/2021, 9:42:12 PM LOG [InstanceLoader] AppModule dependencies initialized +0ms
[Nest] 9994 - 08/09/2021, 9:42:12 PM LOG [InstanceLoader] GraphQLSchemaBuilderModule dependencies initialized +1ms
[Nest] 9994 - 08/09/2021, 9:42:12 PM LOG [InstanceLoader] GraphQLModule dependencies initialized +0ms
[Nest] 9994 - 08/09/2021, 9:42:12 PM LOG [InstanceLoader] TypeOrmCoreModule dependencies initialized +94ms
[Nest] 9994 - 08/09/2021, 9:42:12 PM LOG [RoutesResolver] AppController {/}: +3ms
[Nest] 9994 - 08/09/2021, 9:42:12 PM LOG [RouterExplorer] Mapped {/, GET} route +2ms
[Nest] 9994 - 08/09/2021, 9:42:12 PM LOG [NestApplication] Nest application successfully started +108ms
Great, this means we could connect to the database even though we don’t have any entities set. The synchronize: true will auto generate the tables for us but this is heavily not recommended for production purposes. For that, we need to setup migrations.
Let’s edit our user entity now!
Our previous article used the resource generator from the NestJS cli and it created only an ‘exampleField’. Let’s modify our user.entity for now and add some useful columns such as: userId, firstName, lastName, email, role. As a Typescript class, we could just add the fields but since we already have GraphQL, don’t forget to add the Field annotation.
import { ObjectType, Field, Int } from '@nestjs/graphql';
@Entity()
@ObjectType()
export class User {
@Field(() => String, { description: 'id of the user' })
userId: string;
@Field(() => Int, { description: 'Example field (placeholder)' })
exampleField: number;
@Field(() => String, { description: 'first name of the user' })
firstName: string;
@Field(() => String, { description: 'last name of the user' })
lastName: string;
@Field(() => String, { description: 'email of the user' })
email: string;
@Field(() => String, { description: 'role of the user' })
role: string;
}
Our GraphQL module will understand the new fields for this entity, later we will need to update the user DTO. To start adding the TypeORM, that also uses annotations, we just need to decorate the User with the following:
import { ObjectType, Field, Int } from '@nestjs/graphql';
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
@ObjectType()
export class User {
@PrimaryGeneratedColumn('uuid')
@Field(() => String, { description: 'id of the user' })
userId: string;
@Column('int')
@Field(() => Int, { description: 'Example field (placeholder)' })
exampleField: number;
@Column()
@Field(() => String, { description: 'first name of the user' })
firstName: string;
@Column()
@Field(() => String, { description: 'last name of the user' })
lastName: string;
@Column()
@Field(() => String, { description: 'email of the user' })
email: string;
@Column()
@Field(() => String, { description: 'role of the user' })
role: string;
}
Note that we can have our PrimaryKey as the userId with already set uuid, so we don’t need to worry to pass down this information… I also set some Column values as sake of demonstration but please remember to always have the TypeORM documentation to support your needs. Remember that our service consumes this User entity and our DTO uses similar values that we need to update before running our app.
Let’s first update our create-user.input.ts:
import { InputType, Int, Field } from '@nestjs/graphql';
@InputType()
export class CreateUserInput {
@Field(() => Int, { description: 'Example field (placeholder)' })
exampleField: number;
@Field(() => String, { description: 'first name of the user' })
firstName: string;
@Field(() => String, { description: 'last name of the user' })
lastName: string;
@Field(() => String, { description: 'email of the user' })
email: string;
@Field(() => String, { description: 'role of the user' })
role: string;
}
ps: no need to add the userId since it will be auto generated…
Now, updating our update-user.input.ts:
import { CreateUserInput } from './create-user.input';
import { InputType, Field, Int, PartialType } from '@nestjs/graphql';
@InputType()
export class UpdateUserInput extends PartialType(CreateUserInput) {
@Field(() => String)
userId: string;
}
remember that we are using uuid as id so in that case we are updating our previous id as a number to use userId as a string.
If we go now to our user.service.ts we will see several IDE errors because now it is missing our updated User type. Let’s fix at once meanwhile adding the TypeORM repository that will understand our User entity. For having it there we just need to inject the repository via constructor:
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';...
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {}
We also need to add an import on our users.module.ts that will become like the following:
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersResolver } from './users.resolver';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
@Module({
imports: [TypeOrmModule._forFeature_([User])],
providers: [UsersResolver, UsersService],
})
export class UsersModule {}
Cool, let’s code our CRUD now:
Create an user:
async create(createUserInput: CreateUserInput): Promise<User> {
const user = this.userRepository.create(createUserInput);
return await this.userRepository.save(user);
}
Find an user:
async findOne(userId: string): Promise<User> {
const user = await this.userRepository.findOne(userId);
if (!user) {
throw new NotFoundException(`User #${userId} not found`);
}
return user;
}
List users:
async findAll(): Promise<Array<User>> {
return await this.userRepository.find();
}
Update an user:
async update(
userId: string,
updateUserInput: UpdateUserInput,
): Promise<User> {
const user = await this.userRepository.preload({
userId: userId,
...updateUserInput,
});
if (!user) {
throw new NotFoundException(`User #${userId} not found`);
}
return this.userRepository.save(user);
}
Remove an user:
async remove(userId: string): Promise<User> {
const user = await this.findOne(userId);
await this.userRepository.remove(user);
return {
userId: userId,
firstName: '',
lastName: '',
email: '',
role: '',
exampleField: 0,
};
}
The async functions makes the code more readable and allow us to use the await feature instead of dealing with Promises and “.then” functionality.
If you want to copy and paste the full service, here it go:
import { Injectable, NotFoundException } from '@nestjs/common';
import { CreateUserInput } from './dto/create-user.input';
import { UpdateUserInput } from './dto/update-user.input';
import { User } from './entities/user.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {}
async create(createUserInput: CreateUserInput): Promise<User> {
const user = this.userRepository.create(createUserInput);
return await this.userRepository.save(user);
}
async findAll(): Promise<Array<User>> {
return await this.userRepository.find();
}
async findOne(userId: string): Promise<User> {
const user = await this.userRepository.findOne(userId);
if (!user) {
throw new NotFoundException(`User #${userId} not found`);
}
return user;
}
async update(
userId: string,
updateUserInput: UpdateUserInput,
): Promise<User> {
const user = await this.userRepository.preload({
userId: userId,
...updateUserInput,
});
if (!user) {
throw new NotFoundException(`User #${userId} not found`);
}
return this.userRepository.save(user);
}
async remove(userId: string): Promise<User> {
const user = await this.findOne(userId);
await this.userRepository.remove(user);
return {
userId: userId,
firstName: '',
lastName: '',
email: '',
role: '',
exampleField: 0,
};
}
Now, final point is to update our users.resolver.ts, which we are just going to update the id to userId and make it as a string over a number:
import { Resolver, Query, Mutation, Args, Int } from '@nestjs/graphql';
import { UsersService } from './users.service';
import { User } from './entities/user.entity';
import { CreateUserInput } from './dto/create-user.input';
import { UpdateUserInput } from './dto/update-user.input';
@Resolver(() => User)
export class UsersResolver {
constructor(private readonly usersService: UsersService) {}
@Mutation(() => User)
createUser(@Args('createUserInput') createUserInput: CreateUserInput) {
return this.usersService.create(createUserInput);
}
@Query(() => [User], { name: 'users' })
findAll() {
return this.usersService.findAll();
}
@Query(() => User, { name: 'user' })
findOne(@Args('userId', { type: () => String }) userId: string) {
return this.usersService.findOne(userId);
}
@Mutation(() => User)
updateUser(@Args('updateUserInput') updateUserInput: UpdateUserInput) {
return this.usersService.update(updateUserInput.userId, updateUserInput);
}
@Mutation(() => User)
removeUser(@Args('userId', { type: () => String }) userId: string) {
return this.usersService.remove(userId);
}
}
Let’s run the app:
nestjs-with-graphql git:(002) ✗ yarn start
yarn run v1.22.10
warning ../../package.json: No license field
$ nest start
[Nest] 10399 - 08/09/2021, 10:11:06 PM LOG [NestFactory] Starting Nest application...
[Nest] 10399 - 08/09/2021, 10:11:06 PM LOG [InstanceLoader] TypeOrmModule dependencies initialized +88ms
[Nest] 10399 - 08/09/2021, 10:11:06 PM LOG [InstanceLoader] AppModule dependencies initialized +1ms
[Nest] 10399 - 08/09/2021, 10:11:06 PM LOG [InstanceLoader] GraphQLSchemaBuilderModule dependencies initialized +0ms
[Nest] 10399 - 08/09/2021, 10:11:06 PM LOG [InstanceLoader] GraphQLModule dependencies initialized +1ms
[Nest] 10399 - 08/09/2021, 10:11:06 PM LOG [InstanceLoader] TypeOrmCoreModule dependencies initialized +201ms
[Nest] 10399 - 08/09/2021, 10:11:06 PM LOG [InstanceLoader] TypeOrmModule dependencies initialized +1ms
[Nest] 10399 - 08/09/2021, 10:11:06 PM LOG [InstanceLoader] UsersModule dependencies initialized +0ms
[Nest] 10399 - 08/09/2021, 10:11:06 PM LOG [RoutesResolver] AppController {/}: +4ms
[Nest] 10399 - 08/09/2021, 10:11:06 PM LOG [RouterExplorer] Mapped {/, GET} route +2ms
[Nest] 10399 - 08/09/2021, 10:11:06 PM LOG [NestApplication] Nest application successfully started +101ms
As a double check, I am going to open my Postgres using pgAdmin:
All new columns generated using TypeORM
That’s great! Let’s try a few mutations/queries on our playground at http://localhost:3000/graphql
Creating an user:
mutation {
createUser(
createUserInput: {
exampleField: 1
firstName: "Tks"
lastName: "Sharma"
email: "spam@gmail.com"
role: "ADMIN"
}
) {
userId
exampleField
firstName
lastName
email
role
}
}
response:
{
"data": {
"createUser": {
"userId": "45a5006c-0dcd-4636-a303-7444b84ef7a0",
"exampleField": 1,
"firstName": "Tks",
"lastName": "Sharma",
"email": "spam@gmail.com",
"role": "ADMIN"
}
}
}
New created user
That’s it, we have our first populated user.
I will add a few more changing a few fields and test the list users:
query{
users{
userId
firstName
lastName
email
}
}
response:
{
"data": {
"users": [
{
"userId": "45a5006c-0dcd-4636-a303-7444b84ef7a0",
"firstName": "Tks",
"lastName": "Sharma",
"email": "spam@gmail.com"
},
{
"userId": "b319bfb0-90c5-4faf-893c-cc5535592ec0",
"firstName": "Juliana",
"lastName": "Sharma",
"email": "[dont-spam-me2@gmail.com](mailto:dont-spam-me2@gmail.com)"
},
{
"userId": "83d32cd9-add9-4f34-87a2-5f16d0952f47",
"firstName": "Paulo",
"lastName": "Sharma",
"email": "[dont-spam-me3@gmail.com](mailto:dont-spam-me3@gmail.com)"
}
]
}
}
I will now get one of the generated userId’s and try to update the user. REMEMBER that in your case you need to take the userId that you have since you are not using my local database:
mutation {
updateUser(
updateUserInput: {
userId: "b319bfb0-90c5-4faf-893c-cc5535592ec0"
lastName: "UpdateLastName"
}
) {
userId
firstName
lastName
}
}
response:
{
"data": {
"updateUser": {
"userId": "b319bfb0-90c5-4faf-893c-cc5535592ec0",
"firstName": "Juliana",
"lastName": "UpdateLastName"
}
}
}
That’s it, a basic scaffolding of your database with GraphQL and TypeORM inside your NestJS environment.
Even though this is the baby steps into TypeORM, NestJS and GraphQL, there are a lot of things that we can do meanwhile. One of them is to use a .env file over hardcode. For that, let’s add the .env file in our root folder with the following:
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=postgres
DB_PASSWORD=SuperSecret!23
DB_DATABASE=postgres
and update our TypeORM module to load from there:
TypeOrmModule._forRoot_({
keepConnectionAlive: true,
type: 'postgres',
host: process.env.DB_HOST,
port: +process.env.DB_PORT,
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
autoLoadEntities: true,
synchronize: true,
}),
I am not tackling the Config module of NestJS today but a quick way that works in any nodejs project is to use dotenv to load the dotenv:
yarn add dotenv
then add:
import * as dotenv from 'dotenv';
dotenv.config();
your app.module.ts will be like this:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { GraphQLModule } from '@nestjs/graphql';
import { UsersModule } from './users/users.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import * as dotenv from 'dotenv';
dotenv.config();
@Module({
imports: [
GraphQLModule._forRoot_({
autoSchemaFile: './schema.gql',
debug: true,
playground: true,
}),
TypeOrmModule._forRoot_({
keepConnectionAlive: true,
type: 'postgres',
host: process.env.DB_HOST,
port: +process.env.DB_PORT,
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
autoLoadEntities: true,
synchronize: true,
}),
UsersModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Now you can run without hardcode. With this approach you can have the same project running in the cloud and passing different env variables there without a need to duplicate your code.
In this article we talked about heads on to create a Postgres integration with TypeORM, adding all basic CRUD of an Entity. There are several missing topics here such as:
- Relationship: one to one, one to many, many to one and many to many. Those worths an article just to talk about
- Password: hashing is extremely important here but I would recommend to not use password field in your database if we can use Firebase, Auth0 and others that abstracts our security issues
- Listing users with pagination… This is quite straightforward to add but I let the creativity to you. Just follow the idea of using a generic pagination DTO that contains the page + limit and use TypeORM documentation.
Comments