NestJS(+ NodeJS) + GraphQL

캡틴 노드랭크·2021년 9월 16일
1

NestJS

목록 보기
2/5

이번엔 NestJS와 GraphQL로 간단한 User API를 작성해보고자 한다.

마이클 과이의 영상을 기반으로 작업했으며, 대충 어떤식으로 작성하는지 간단히 이해하고자 글을 쓴다.

프로젝트 생성

npm i -g @nestjs/cli
nest new nestjs-graphql

프로젝트의 폴더명 nestjs-graphql를 생성해준다.

의존성 라이브러리 및 파일 정리

이제 프로젝트 진행에 앞서 필요한 라이브러리와 필요없는 파일들은 지워준다.

파일은 대충 정리 되었으니, 이 프로젝트에서 필요한 라이브러리를 설치 해준다.

npm install @nestjs/graphql graphql graphql-tools class-validator uuid apollo-server-express

npm install을 사용했기 때문에 모든 의존성 라이브러리는 package.jsondependencies에 적용된다.

  "dependencies": {
    "@nestjs/common": "^8.0.0",
    "@nestjs/core": "^8.0.0",
    "@nestjs/graphql": "^9.0.4",
    "@nestjs/platform-express": "^8.0.0",
    "apollo-server-express": "^3.3.0",
    "class-validator": "^0.13.1",
    "graphql": "^15.5.3",
    "graphql-tools": "^8.2.0",
    "reflect-metadata": "^0.1.13",
    "rimraf": "^3.0.2",
    "rxjs": "^7.2.0",
    "uuid": "^8.3.2"
  },

체계적으로 구분짓기

Nest JS + GraphQL환경에서 어떠한 API를 구현하기 위해서는 반드시 지켜줘야할 규칙이 존재한다. 유투브 영상에서와 같이 User Model,module,service,resolver 등 확실하게 구분지어 깔끔한 코드를 작성함과 동시에 유지 보수성을 향상시킨다.

이 방법은 공식 페이지 Overview -> Providers 혹은 Modules 탭에서도 디렉토리 트리가 어떻게 구성되어있는지, 되어야하는지 확인할 수 있다.

작성 규칙

Code First

  • 코드 우선 접근 방식에선 SDL, Schema Definition Language을 작성하여 GPL 스키마를 만드는 일반적인 프로세스를 따르지 않는다.

TypeScript 데코레이터

  • TypeScript 데코레이터를 사용하여, TypeScript 클래스 정의에 SDL을 생성한다.

  • @nestjs/graphql패키지는 데코레이터를 통해 정의된 메타데이터를 읽고 자동으로 스키마를 생성한다.

공식홈페이지 예시

import { Field, Int, ObjectType } from '@nestjs/graphql';
import { Post } from './post';

@ObjectType()  //데코레이터
export class Author {
  @Field(type => Int)
  id: number;

  @Field({ nullable: true })
  firstName?: string;

  @Field({ nullable: true })
  lastName?: string;

  @Field(type => [Post])
  posts: Post[];
}
  • nullable: 필드가 nullable인지 여부를 지정: Boolean
  • description: 필드설명을 설정: string
  • deprecationReason: 필드를 사용되지 않음을 표시: string

필드가 배열인 경우

  @Field(type => [Post])
  posts: Post[];

반드시 []를 사용하여 명시해줘야한다.

작업 시작

app.module.ts

import { Module } from '@nestjs/common';
//nestJS에서 GQL을 사용하기위한 방법
import { GraphQLModule } from '@nestjs/graphql';
import { UsersModule } from './users/users.module';

@Module({
  imports: [
    GraphQLModule.forRoot({
      autoSchemaFile: true,
      playground: true,
    }),
    UsersModule,
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}

NestJS에서 GraphQL을 활용하기 위해@nestjs/graphql라는 모듈을 사용하는데 중점을 둔다.

한참위에서 종속성을 가진 라이브러리를 설치하기위해 Apollo-server-express를 설치해줬지만.. 정작 사용하진 않고있다.

GraphQLModule에서 forRoot메소드는 객체를 하나의 인자로 받는데, 공식 홈페이지에서 나온대로 여러가지 옵션이 존재하며, 이 옵션들은 Apollo Instance로 전달되기 때문에 a-s-e를 설치해 준게 아닐까 싶다.

이건 내 단순 추측이므로 정확하게 알기 위해서는 StackOverflow에 질문던져봐야겠다.
여기서 두 가지 옵션을 사용했는데, 다른 옵션은 나중에 작성하겠다.

autoSchemaFile부터 알아보면, 자동으로 생성한 스키마를 어디에 저장할 것인가를 결정한다.

  • 특정 경로, 특정 파일이 아닌 메모리에 생성하고 싶을 경우 true
GraphQLModule.forRoot({
  autoSchemaFile: true,
}),
  • 현재 경로/src/schema.gql에 자동 생성된 스키마를 저장하고 싶다.
GraphQLModule.forRoot({
  autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
}),

여기서 process.cwd()는 node명령을 호출한 현재 작업디렉터리의 경로를 나타낸다.

  • 기본적으로 생성된 스키마를 사전순으로 정렬할때,
GraphQLModule.forRoot({
  autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
  sortSchema: true,
}),

app.module.ts./src 디렉토리 내의 루트 모듈 파일이므로, 현재로선 providerscontrollers는 필요하지 않기때문에 [] 빈배열로 비워둔다.

dto/args

get-user.args.ts

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

@ArgsType()
export class GetUserArgs {
  @Field()
  @IsNotEmpty()
  userId: string;
}

get-users.args.ts

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

@ArgsType()
export class GetUsersArgs {
  @Field(() => [String])
  @IsArray()
  userIds: string[];
}

dto/input

create-user.input.ts

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

@InputType()
export class CreateUserInput {
  @Field()
  @IsNotEmpty()
  @IsEmail()
  email: string;

  @Field(() => Int)
  @IsNotEmpty()
  age: number;

  @Field()
  @IsNotEmpty()
  password: string;
}

delete-user.input.ts

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

@InputType()
export class DeleteUserInput {
  @Field()
  @IsNotEmpty()
  userId: string;
}

update-user.input.ts

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

@InputType()
export class UpdateUserInput {
  @Field()
  @IsNotEmpty()
  @IsEmail()
  userId: string;

  @Field()
  @IsOptional()
  @IsNotEmpty()
  age?: number;

  @Field({ nullable: true })
  @IsOptional()
  isSubscribed?: boolean;
}

models/user.ts

import { Field, Int, ObjectType } from '@nestjs/graphql';

@ObjectType()
export class User {
  @Field()
  userId: string;

  @Field()
  email: string;

  @Field(() => Int)
  age: number;

  @Field({ nullable: true })
  isSubscribed?: boolean;

  @Field({ nullable: true })
  password?: string;
}

user.module.ts

import { Module } from '@nestjs/common';
import { UsersResolver } from './users.resolver';
import { UsersService } from './users.service';

@Module({
  providers: [UsersResolver, UsersService],
  exports: [UsersService],
})
export class UsersModule {}

user.resolver.ts

import { Resolver, Query, Args, Mutation } from '@nestjs/graphql';
import { GetUserArgs } from './dto/args/get-user.args';
import { GetUsersArgs } from './dto/args/get-users.args';
import { CreateUserInput } from './dto/input/create-user.input';
import { DeleteUserInput } from './dto/input/delete-user.input';
import { UpdateUserInput } from './dto/input/update-user.input';
import { User } from './models/user';
import { UsersService } from './users.service';

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

  @Query(() => User, { name: 'user', nullable: true })
  getUser(@Args() getUserArgs: GetUserArgs): User {
    return this.usersService.getUser(getUserArgs);
  }

  @Query(() => [User], { name: 'users', nullable: 'items' })
  getUsers(@Args() getUsersArgs: GetUsersArgs): User[] {
    return this.usersService.getUsers(getUsersArgs);
  }

  @Mutation(() => User)
  createUser(@Args('createUserData') createUserData: CreateUserInput): User {
    return this.usersService.createUser(createUserData);
  }

  @Mutation(() => User)
  updateUser(@Args('updateUserData') updateUserData: UpdateUserInput): User {
    return this.usersService.updateUser(updateUserData);
  }

  @Mutation(() => User)
  deleteUser(@Args('deleteUserData') deleteUserData: DeleteUserInput): User {
    return this.usersService.deleteUser(deleteUserData);
  }
}

user.service.ts

import { Injectable } from '@nestjs/common';
import { v4 as uuidv4 } from 'uuid';
import { GetUserArgs } from './dto/args/get-user.args';
import { GetUsersArgs } from './dto/args/get-users.args';
import { CreateUserInput } from './dto/input/create-user.input';
import { DeleteUserInput } from './dto/input/delete-user.input';
import { UpdateUserInput } from './dto/input/update-user.input';
import { User } from './models/user';

@Injectable()
export class UsersService {
  private users: User[] = [];

  public createUser(createUserData: CreateUserInput): User {
    const user: User = {
      userId: uuidv4(),
      ...createUserData,
    };

    this.users.push(user);

    return user;
  }

  public updateUser(updateUserData: UpdateUserInput): User {
    const user = this.users.find(
      (user) => user.userId === updateUserData.userId,
    );

    Object.assign(user, updateUserData);

    return user;
  }

  public getUser(getUserArgs: GetUserArgs): User {
    return this.users.find((user) => user.userId === getUserArgs.userIds);
  }

  public getUsers(getUsersArgs: GetUsersArgs): User[] {
    return getUsersArgs.userIds.map((userId) => this.getUser({ userId }));
  }

  public deleteUser(deleteUserData: DeleteUserInput): User {
    const userIdx = this.users.findIndex(
      (user) => user.userId === deleteUserData.userId,
    );

    const user = this.users[userIdx];

    this.users.splice(userIdx);
    return user;
  }
}

PlayGround

mutationRESTAPIPOST, PETCH, PUT의 역활을 하는 것 같고, QueryGET처럼 정보를 가져올 수 있다.

createUser를 작성하면 userId에 고유한 uuid를 발급하여 담아준다.

이제 간단한 USER API는 작성했으니 typeORMmysql, mongodb등을 활용하여 db를 관리하는 방향으로 진행하겠다.

ref:마이클과이

profile
다시 처음부터 천천히... 급할필요가 없다.

0개의 댓글