Skip to content

[Startup MVP recipes #5.1] A simple resource generated by nest-cli then configured (part 1)

Preparation

Install nest cli (globally)

npm install -g @nestjs/cli

Install class-validator for custom validations.

npm install class-validator

Generate the code

Generate a resource, with generated CRUD endpoints (code first GraphQL)

nest g resource

Set the name to users

Select “GraphQL (code first)”

Generate CRUD entry points? → Yes

The generated code serves a good bare bone to start working with, but it may not comply to our lint and formatting settings. We can fix them along the way.

From my understanding Nest want to be ORM neutral so it didn’t generate anything related to TypeORM and therefore we can only see example definitions and the service is yet to be completed by us to connect to TypeORM repos.

Entity Definition (w/ GQL fields and validation)

Let’s create the entity file which defines which columns the entity model should contain in postgres SQL db and what fields to expose on GQL queries.

In the meantime let’s add custom validations to the fields too, right in the same file.

Take this minimal version of User entity as example:

import { Field, ObjectType } from '@nestjs/graphql';
import { IsEmail } from 'class-validator';
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@ObjectType()
@Entity()
export default class User {
  @PrimaryGeneratedColumn('uuid')
  @Field(() => String, { description: 'Generated Primary UUID column' })
  uuid: string;

  @Column({ unique: true })
  @Field(() => String, { description: `User's email address` })
  @IsEmail()
  email: string;
}

We use decorators to pass information to corresponding libraries. The decorator will take the TS class member as parameter in.

When looking at this example, the important thing to keep an eye on is which decorators are imported from which package:

  • @Field() and @ObjectType() are from the graphql package
    • ObjectType creates a new object in the gql schema
    • field is straightforward
    • The result will be compiled into src/schema.gql as we defined in previous chapter on the GraphQL module
    • We can later examine the Object Type and its fields in Apollo Studio schema section, the descriptions we wrote will also show up there
  • @Column() and @PrimaryGeneratedColumn() are imported from TypeORM. It tells TypeORM to define the column in the table. @PrimaryGeneratedColumn() will define the column as primary key, and if we pass in 'uuid' then it will generate uuid as id for this entity (if not then it will be auto-increment id from 1,2,3… ).
  • @IsEmail() serves as the validator for email input. It will be automatically applied on every query/mutation that uses the definition on this object type (but if we have a separate input type (to be covered), we should add the decorator again).

Optionally we can use cli plugin to simply the decorator declarations, but I didn’t use it in the project to keep the code standardized with the community (https://docs.nestjs.com/graphql/cli-plugin)

Since we have used the code generator and our module is automatically linked into app module, if we run this above code locally, and with synchronize on we will see the table in our local postgres db. (Check https://jczhang.com/2022/07/09/mvp-recipes-1-nest-js-local-dev-environment-setup/ again if necessary and optionally you can install tools like DBeaver to connect to the local db and see what the table actually looks like)

The CreateUserInput DTO

From Okta: (I just randomly googled it), in my opinion it serves as the interface.

https://www.okta.com/identity-101/dto/#:~:text=A%20data%20transfer%20object%20(DTO,for%20people%20with%20programming%20backgrounds.

A data transfer object (DTO) is an object that carries data between processes. You can use this technique to facilitate communication between two systems (like an API  and your server) without potentially exposing sensitive information.

In our minimal case, we just need the email address in the DTO to create the user.

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

@InputType()
export default class CreateUserInput {
  @Field(() => String, { description: `User's email address` })
  @IsEmail()
  email: string;
}

Resolver, Service and TypeORM repo

Now let’s configure the UserService to connect to TypeORM repository API of the User entity. (https://typeorm.io/working-with-repository)

In User Module import it:

// user.module.ts
import { TypeOrmModule } from '@nestjs/typeorm';

//..
imports: [TypeOrmModule.forFeature([User])],

Nest uses dependency injection and will automatically inject the repo into the constructor of the service. (We will cover DI in details later)

Declare the create-user mutation in resolver.

// users.resolver.ts

@Resolver(() => User)
export default class UsersResolver {
  constructor(private readonly usersService: UsersService) {}

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

Inject TypeORM repo into UsersService and implement the create() logic of UsersService

// users.service.ts

@Injectable()
export default class UsersService {
  constructor(
    @InjectRepository(User)
    private userRepo: Repository<User>,
  ) {}

  async create(createUserInput: CreateUserInput) {
    const { email } = createUserInput;
    return this.userRepo.save({
      email,
    });
  }
}

Test the createUser GQL mutation

Now in Apollo Studio (i.e. http://localhost:3000/graphql))

Test the createUser mutation

mutation Mutation($createUserInput: CreateUserInput!) {
  createUser(createUserInput: $createUserInput) {
    email
    id
  }
}

Variables JSON:

{
  "createUserInput": {
    "email": "james@example.com"
  }
}

Here is an example response

{
  "data": {
    "createUser": {
      "email": "james2@example.com",
      "id": "968b7d62-6010-4d84-8e0d-b901aca41696"
    }
  }
}

Congrats for your first minimal GraphQL mutation.

Leave a Reply

Your email address will not be published. Required fields are marked *