Kh.
KyuHyuk Blog

Nest.js로 GraphQL 서버 구축하기

GraphQL

Nest.js에 GraphQL 서버를 구축하는 방법을 정리해보았습니다.

Nest.js 프로젝트 생성

npm init를 사용하여 아래와 같이 package.json을 생성합니다.
Create NestJS Project

아래의 패키지를 설치합니다.

npm install --save @nestjs/axios @nestjs/common @nestjs/core @nestjs/platform-express reflect-metadata rxjs
npm install --save-dev @nestjs/cli @nestjs/schematics @types/express @types/node ts-loader ts-node tsconfig-paths typescript

package.json"main": "index.js" 행을 삭제하고 아래 내용을 추가합니다.

  "private": true,
  "scripts": {
    "build": "nest build",
    "start": "nest start",
    "start:dev": "NODE_ENV=development nest start --watch",
    "start:debug": "NODE_ENV=development nest start --debug --watch",
    "start:prod": "npm run build && NODE_ENV=production node dist/main"
  },

nest-cli.json, tsconfig.build.json, tsconfig.json을 아래와 같이 작성하여 저장합니다.

nest-cli.json :

{
  "collection": "@nestjs/schematics",
  "sourceRoot": "src"
}

tsconfig.build.json :

{
  "extends": "./tsconfig.json",
  "exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
}

tsconfig.json :

{
  "compilerOptions": {
    "module": "commonjs",
    "declaration": true,
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "target": "es2017",
    "sourceMap": true,
    "outDir": "./dist",
    "baseUrl": "./",
    "incremental": true,
    "skipLibCheck": true
  }
}

Nest.js 프로젝트에 GraphQL 추가 및 설정

아래의 패키지를 설치합니다.

npm install --save @nestjs/graphql apollo-server-express graphql

src 폴더를 만들고 src/app.module.tssrc/main.ts를 생성합니다.

src/app.module.ts :

import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';

@Module({
  imports: [GraphQLModule.forRoot({})],
})
export class AppModule {}

위의 코드에서 forRoot()여러 설정을넣을 수 있습니다. 설정값은기본 Apollo 인스턴스로 전달됩니다.
예를 들어 playgrounddebug를 끄려면 다음과 같이 사용할 수 있습니다.

import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';

@Module({
  imports: [
    GraphQLModule.forRoot({
      debug: false,
      playground: false,
    }),
  ],
})
export class AppModule {}

src/main.ts :

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(8080);
}
bootstrap();

Resolver, Schema 생성

아마 위의 코드를 모두 작성하고 npm run start:dev를 하면 UnhandledPromiseRejectionWarning: Error: Apollo Server requires either an existing schema, modules or typeDefs라고 오류가 발생할 것입니다.

이 오류가 발생하는 이유는 Resolver, Schema가 없어서 발생하는 오류입니다.
여기서 말하는 Resolver는 GraphQL 작업(Query, Mutation, Subscription)을 데이터로 변환하기 위해 사용됩니다.

Schema는 아래와 같이 정의할 수 있습니다. 간단하게 주소록에 필요한 Schema를 작성해보겠습니다.

graphql 폴더를 생성하고 person.graphql 파일을 생성합니다.

graphql/person.graphql :

type Person {
  id: String!
  name: String!
  number: String
  email: String
}

type Query {
  getAllPerson: [Person]
}

GraphQL에서 사용되는 Schema와 Type이 궁금하다면 https://graphql.org/learn/schema를 읽어보세요.

그리고 src/app.module.tstypePaths를 추가하여 .graphql 확장자를 가진 파일을 GraphQL의 Schema 로 사용하도록 설정합니다.

src/app.module.ts :

import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';

@Module({
  imports: [
    GraphQLModule.forRoot({
      typePaths: ['./**/*.graphql'],
    }),
  ],
})
export class AppModule {}

Schema를 만들었으니 이제 Resolver를 만들어 봅시다.

src 폴더 안에 models 폴더를 생성하고, src/models/person.resolver.tssrc/models/person.module.ts 파일을 작성합니다.
아직 데이터베이스와 연결하지 않았기 때문에 getAllPerson Query 요청이 오면 아래와 같은 데이터를 반환하도록 작성합니다.

src/models/person.resolver.ts :

import { Query, Resolver } from '@nestjs/graphql';

@Resolver('Person')
export class PersonResolver {
  @Query()
  async getAllPerson() {
    return [
      {
        id: '1',
        name: '이규혁',
        number: '+82 10-1234-5678',
        email: '[email protected]',
      },
      { id: '2', name: '변정원', number: '+82 10-8765-4321' },
    ];
  }
}

src/models/person.module.ts :

import { Module } from '@nestjs/common';
import { PersonResolver } from './person.resolver';

@Module({
  providers: [PersonResolver],
})
export class PersonModule {}

src/app.module.tsPersonModule를 추가합니다.

src/app.module.ts :

import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { PersonModule } from './models/person.module';

@Module({
  imports: [
    GraphQLModule.forRoot({
      typePaths: ['./**/*.graphql'],
    }),
    PersonModule,
  ],
})
export class AppModule {}

npm run start:dev를 실행하고 http://localhost:8080/graphql에 접속합니다.
GraphQL Playground가 브라우저에 출력되면, 아래와 같이 입력하여 실행해봅니다.

우리가 의도한 대로 동작하는 것을 확인할 수 있습니다.
GraphQL Playground

데이터베이스와 연결

이 글에서는 MongoDB를 GraphQL과 연결하도록 하겠습니다.
https://www.mongodb.com/try/download/community에접속하여 MongoDB Community Server를 다운로드하고 설치합니다.

간편하게 MongoDB를 사용하기 위해서 Robo 3T를 사용하도록 하겠습니다.
아래와 같이 데이터 몇 개를 삽입하였습니다.
Robo 3T - Insert Document

Nest.js 프로젝트에 아래의 패키지를 추가합니다.

npm install --save mongoose

src/database.providers.tssrc/database.module.ts 파일을 작성하여, 데이터베이스와의 연결을 설정합니다.

src/database.providers.ts :

import * as mongoose from 'mongoose';

const DATABASE_NAME = 'GraphQL';

export const databaseProviders = [
  {
    provide: 'DATABASE_CONNECTION',
    useFactory: (): Promise<typeof mongoose> =>
      mongoose.connect(`mongodb://localhost/${DATABASE_NAME}`),
  },
];

src/database.module.ts :

import { Module } from '@nestjs/common';
import { databaseProviders } from './database.providers';

@Module({
  providers: [...databaseProviders],
  exports: [...databaseProviders],
})
export class DatabaseModule {}

Mongoose를 사용하면 모든 것이 Schema에서 파생됩니다.
srcschemas 폴더를 만들고, person.schema.ts 파일을 작성합니다.

src/schemas/person.schema.ts :

import { Field, ObjectType } from '@nestjs/graphql';
import * as mongoose from 'mongoose';
import { Document } from 'mongoose';

export const PersonSchema = new mongoose.Schema({
  _id: { type: mongoose.Schema.Types.ObjectId, auto: true },
  name: String,
  number: String,
  email: String,
});

@ObjectType()
export class Person extends Document {
  @Field()
  _id: string;

  @Field()
  name: string;

  @Field()
  number: string;

  @Field()
  email: string;
}

위에서 만든 PersonSchema를 사용하여 Model Provider를 만들어봅시다.
아래와 같이 person.providers.ts 파일을 작성합니다.

src/models/person.providers.ts :

import { Connection } from 'mongoose';
import { PersonSchema } from '../schemas/person.schema';

export const PersonProviders = [
  {
    provide: 'PERSON_MODEL',
    useFactory: (connection: Connection) => connection.model('Person', PersonSchema, 'Person'),
    inject: ['DATABASE_CONNECTION'],
  },
];

PERSON_MODELDATABASE_CONNECTIONconstants.ts 파일을 따로 만들어 분리하여 보관하는 것을권장합니다.

이제 @Inject()를 사용하여 PersonServicePERSON_MODEL를 추가합니다.
src/models/person.service.ts 파일을 아래와 같이 작성합니다.

src/models/person.service.ts :

import { Model } from 'mongoose';
import { Injectable, Inject } from '@nestjs/common';
import { Person } from '../schemas/person.schema';

@Injectable()
export class PersonService {
  constructor(
    @Inject('PERSON_MODEL')
    private personModel: Model<Person>
  ) {}

  async findAll(): Promise<Person[]> {
    return this.personModel.find().exec();
  }
}

src/models/person.module.tsDatabaseModulePersonService, PersonProviders를 추가합니다.

src/models/person.module.ts :

import { Module } from '@nestjs/common';
import { DatabaseModule } from '../database.module';
import { PersonProviders } from './person.providers';
import { PersonResolver } from './person.resolver';
import { PersonService } from './person.service';

@Module({
  imports: [DatabaseModule],
  providers: [PersonResolver, PersonService, ...PersonProviders],
})
export class PersonModule {}

기존에 작성했던 src/models/person.resolver.ts를 아래와 같이 수정하여 실제 데이터베이스에 있는 데이터를 가져오도록 구현합니다.

import { Query, Resolver } from '@nestjs/graphql';
import { Person } from 'src/schemas/person.schema';
import { PersonService } from './person.service';

@Resolver('Person')
export class PersonResolver {
  constructor(private personService: PersonService) {}

  @Query(() => [Person])
  async getAllPerson() {
    return await this.personService.findAll();
  }
}

켜져 있는 NestJS 서버를 중단하고, npm run start:dev를 다시 실행한 뒤 http://localhost:8080/graphql에 접속합니다.

Query를 요청하면, 아래와 같이 MongoDB에 있는 데이터가 정상적으로 나오고 있음을 확인할 수 있습니다.
GraphQL Playground

Query를 구현했으니 이제 Mutation을 구현해봅시다.

graphql/person.graphql에 아래와 같이 Mutation을 추가합니다.

graphql/person.graphql :

type Person {
  id: String!
  name: String!
  number: String
  email: String
}

input CreatePersonInput {
  name: String!
  number: String
  email: String
}

type Query {
  getAllPerson: [Person]
}

type Mutation {
  addPerson(person: CreatePersonInput): Person
}

src/schemas/person.schema.ts에도 CreatePersonInput을 추가합니다.

src/schemas/person.schema.ts :

import { Field, ObjectType } from '@nestjs/graphql';
import * as mongoose from 'mongoose';
import { Document } from 'mongoose';

export const PersonSchema = new mongoose.Schema({
  _id: { type: mongoose.Schema.Types.ObjectId, auto: true },
  name: String,
  number: String,
  email: String,
});

@ObjectType()
export class Person extends Document {
  @Field()
  _id: string;

  @Field()
  name: string;

  @Field()
  number: string;

  @Field()
  email: string;
}

@ObjectType()
export class CreatePersonInput {
  @Field()
  name: string;

  @Field()
  number: string;

  @Field()
  email: string;
}

PersonServiceaddPerson()을 추가합니다.

src/models/person.service.ts :

import { Model } from 'mongoose';
import { Injectable, Inject } from '@nestjs/common';
import { CreatePersonInput, Person } from '../schemas/person.schema';

@Injectable()
export class PersonService {
  constructor(
    @Inject('PERSON_MODEL')
    private personModel: Model<Person>
  ) {}

  async findAll(): Promise<Person[]> {
    return this.personModel.find().exec();
  }

  async addPerson(person: CreatePersonInput): Promise<Person> {
    return this.personModel.create(person);
  }
}

PersonResolveraddPerson Mutation을 추가합니다.

src/models/person.resolver.ts :

import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { CreatePersonInput, Person } from '../schemas/person.schema';
import { PersonService } from './person.service';

@Resolver('Person')
export class PersonResolver {
  constructor(private personService: PersonService) {}

  @Query(() => [Person])
  async getAllPerson() {
    return await this.personService.findAll();
  }

  @Mutation(() => Person)
  async addPerson(@Args('person') person: CreatePersonInput) {
    return await this.personService.addPerson(person);
  }
}

켜져 있는 NestJS 서버를 중단하고, npm run start:dev를 다시 실행한 뒤 http://localhost:8080/graphql에 접속합니다.

Mutation을 요청하면, 아래와 같이 MongoDB에 정상적으로 데이터가 삽입된 것을 확인할 수 있습니다.
GraphQL Playground
Robo 3T - Documents