이번엔 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.json
의 dependencies
에 적용된다.
"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
탭에서도 디렉토리 트리가 어떻게 구성되어있는지, 되어야하는지 확인할 수 있다.
SDL, Schema Definition Language
을 작성하여 GPL
스키마를 만드는 일반적인 프로세스를 따르지 않는다. 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[];
반드시 []
를 사용하여 명시해줘야한다.
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
디렉토리 내의 루트 모듈 파일이므로, 현재로선 providers
와 controllers
는 필요하지 않기때문에 []
빈배열로 비워둔다.
import { ArgsType, Field } from '@nestjs/graphql';
import { IsNotEmpty } from 'class-validator';
@ArgsType()
export class GetUserArgs {
@Field()
@IsNotEmpty()
userId: string;
}
import { ArgsType, Field } from '@nestjs/graphql';
import { IsArray } from 'class-validator';
@ArgsType()
export class GetUsersArgs {
@Field(() => [String])
@IsArray()
userIds: string[];
}
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;
}
import { Field, InputType } from '@nestjs/graphql';
import { IsNotEmpty } from 'class-validator';
@InputType()
export class DeleteUserInput {
@Field()
@IsNotEmpty()
userId: string;
}
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;
}
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;
}
import { Module } from '@nestjs/common';
import { UsersResolver } from './users.resolver';
import { UsersService } from './users.service';
@Module({
providers: [UsersResolver, UsersService],
exports: [UsersService],
})
export class UsersModule {}
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);
}
}
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;
}
}
mutation
은 RESTAPI
의 POST
, PETCH
, PUT
의 역활을 하는 것 같고, Query
는 GET
처럼 정보를 가져올 수 있다.
createUser
를 작성하면 userId
에 고유한 uuid
를 발급하여 담아준다.
이제 간단한 USER API는 작성했으니 typeORM
과 mysql, mongodb
등을 활용하여 db를 관리하는 방향으로 진행하겠다.
ref:마이클과이