이번에는 Typeorm과 Graphql 사용해 CRU API 구현해보자
이전에는 실제 데이터와 연결만 진행했지만 이번에는 API를 이용해서 데이터베이스에 저장하고 저장한 데이터를 읽어와서 수정까지 진행해보자
이번에는 카테고리에 해당하는 상품을 등록해보자
section10 폴더 안에 10-01-mysql-relation 폴더를 복붙하여
사본 폴더의 이름을 10-03-typeorm-crud 폴더로 수정해준다.
10-03-typeorm-crud → src → apis → products 폴더에
products.module.ts, products.resolver.ts, products.service.ts 파일 총 3개를 먼저 만들어 준다.
products.module.ts 파일 먼저 확인해보자
// products.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Product } from './entities/product.entity';
import { ProductsResolver } from './products.resolver';
import { ProductsService } from './products.service';
@Module({
imports: [
TypeOrmModule.forFeature([ // 엔티티를 전달하는 부분
Product, //
]),
],
providers: [
ProductsResolver, //
ProductsService,
],
})
export class ProductsModule {}
TypeOrmModule.forFeature메서드는 엔티티를 전달한다.ProductsResolver, ProductsService 을 주입해준다.Board 만들 때 데이터 전송 객체. 즉, 네트워크 간에 데이터를 어떤 식으로 보낼지를 정의한 객체를 정의하기 위해서 DTO 를 적용했었다.
이번에는 미리 DTO 를 정의해서 Resolver와 Service에 적용하겠다.
10-03-typeorm-crud → src → apis → products 폴더 안에 dto 폴더를 생성하고
dto 폴더 안에 create-product.input.ts 파일을 만들어준다.
// create-product.input.ts
import { Field, InputType, Int } from '@nestjs/graphql';
@InputType() // 입력받기 때문에 ObjectType이 아님
export class CreateProductInput {
@Field(() => String)
name: string;
@Field(() => String)
description: string;
@Field(() => Int)
price: number;
}
InputType 을 사용한다.ObjectType 을 사용하면 에러가 발생한다.이제 다시 돌아와서 products.resolver.ts 을 만들어 보자
// products.resolver.ts
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { CreateProductInput } from './dto/create-product.input';
import { Product } from './entities/product.entity';
import { ProductsService } from './products.service';
@Resolver()
export class ProductsResolver {
constructor(
private readonly productsService: ProductsService, // 생성자로 의존성 받아주기
) {}
@Mutation(() => Product)
createProduct(
@Args('createProductInput') createProductInput: CreateProductInput, // Args를 통해 전달
): Promise<Product> {
// << 브라우저에 결과 보내는 2가지 방법 >>
// 1. 저장된 객체 그대로 돌려보내주기 => 프론트엔드 개발자분이 브라우저에 임시저장(캐시) 해놓을 수 있음
return this.productsService.create({ createProductInput });
// 2. 결과메시지만 보내주기
// return '정상적으로 카테고리가 등록되었습니다.';
}
}
@Args로 전달받은 데이터를 객체 요소 하나하나의 타입을 createProductInput 을 통해서 유효성 검사를 한다.@Mutation(() => Product) : Mutation의 반환 결과를 Product 타입으로 지정해 주었다.productsService를 의존성 주입받아 create 함수를 사용할 수 있게 되었다.products.service.ts 파일로부터 전달 받은 상품 객체를 return을 통해 브라우저로 전달된다.이제 products.service.ts 파일을 만들어 보자.
// products.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Product } from './entities/product.entity';
import {
IProductsServiceCreate,
} from './interfaces/products-service.interface';
@Injectable()
export class ProductsService {
constructor(
@InjectRepository(Product)
private readonly productsRepository: Repository<Product>,
) {}
create({ createProductInput }: IProductsServiceCreate): Promise<Product> {
const result = this.productsRepository.save({
...createProductInput,
// 하나 하나 직접 나열하는 방식
// name: '마우스',
// description: '좋은 마우스',
// price: 3000,
});
return result;
// {id: akljdfq-1283aad,
// name: "마우스",
// description: "좋은 마우스",
// price: 3000}
}
}
constructor를 사용하여 Product 엔티티를 비즈니스 로직에 의존성 주입을 했다.async ~ await : SQL Query문으로 변환되어 DB로 들어가 저장될 때까지 기다려 줘야하기에 async ~ await를 사용해야 한다.products.resolver.ts 파일로 돌아가야하기에 return을 사용했다.10-03-typeorm-crud → src → apis → products에 interfaces 폴더를 생성 후 products-service.interface.ts 파일을 생성해준다.
// products-service.interface.ts
import { CreateProductInput } from '../dto/create-product.input';
export interface IProductsServiceCreate {
createProductInput: CreateProductInput;
}
interface를 이용해 createProductInput의 타입을 지정해 준다.
app.module.ts 파일에서 지금까지 만든 ProductModule 을 조립하자
// app.module.ts
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BoardsModule } from './apis/boards/boards.module';
import { ConfigModule } from '@nestjs/config';
import { ProductsCategoriesModule } from './apis/productsCategories/productsCategories.module';
import { ProductsModule } from './apis/products/products.module';
@Module({
imports: [
BoardsModule,
ProductsModule, //
ProductsCategoriesModule,
ConfigModule.forRoot(),
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: 'src/commons/graphql/schema.gql',
}),
TypeOrmModule.forRoot({
type: process.env.DATABASE_TYPE as 'mysql',
host: process.env.DATABASE_HOST,
port: Number(process.env.DATABASE_PORT),
username: process.env.DATABASE_USERNAME,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_DATABASE,
entities: [__dirname + '/apis/**/*.entity.*'],
synchronize: true,
logging: true,
}),
],
})
export class AppModule {}
GraphQL API를 만들고 있기 때문에 product.entity.ts 에 graphql 타입을 만들어 줘야한다.
아래와 같이 graphql 타입을 만들어준다.
// product.entity.ts
import { Field, Int, ObjectType } from '@nestjs/graphql';
import { ProductCategory } from 'src/apis/productsCategories/entities/productCategory.entity';
import { ProductSaleslocation } from 'src/apis/productsSaleslocations/entities/productSaleslocation.entity';
import { ProductTag } from 'src/apis/productsTags/entities/productTag.entity';
import { User } from 'src/apis/users/entities/user.entity';
import {
Column,
Entity,
JoinColumn,
JoinTable,
ManyToMany,
ManyToOne,
OneToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity()
@ObjectType()
export class Product {
@PrimaryGeneratedColumn('uuid')
@Field(() => String)
id: string;
@Column()
@Field(() => String)
name: string;
@Column()
@Field(() => String)
description: string;
@Column()
@Field(() => Int)
price: number;
@Column({ default: false }) // 초기값을 지정할 수 있음
@Field(() => Boolean)
isSoldout: boolean;
@JoinColumn()
@OneToOne(() => ProductSaleslocation)
@Field(() => ProductSaleslocation)
productSaleslocation: ProductSaleslocation;
@ManyToOne(() => ProductCategory)
@Field(() => ProductCategory)
productCategory: ProductCategory;
@ManyToOne(() => User)
@Field(() => User)
user: User;
@JoinTable()
@ManyToMany(() => ProductTag, (productTags) => productTags.products)
@Field(() => [ProductTag])
productTags: ProductTag[];
}
@ObjectType(), @Field 추가한다.isSoldOut 컬럼은 판매여부를 기록하는 컬럼이기 때문에, 기본 고정 값( default 값 )을 판매가 되지 않은 상태인 false로 지정하기 위해서 @Column({ default: false }) 추가한다.ProductTag[]로 작성해주었지만, graphql에서는 배열타입을 [ProductTag] 로 작성한다.createProduct API 를 요청하게 되면 데이터가 등록이 되고,
등록된 데이터가 Product type 으로 리턴되어서 나타나게 된다는 것을 API를 만들면서 알게 되었다.
여기서 Product 에 대한 graphql type 을 아직 만들어주지 않았기 때문에 code-first 기능을 활용하여 entity 를 만들어 준 것이다.
즉, entity를 따로 만들어 주었기에 API를 실행하면 graphql type 이 Product 로 나오게 되는 것 이다.
하지만, Product entity를 보면 ProductSaleslocation, ProductCategory, User, ProductTag 컬럼이 존재하는데 현재 4가지 entity 모두 graphql type이 없다.
이런 상태로 실행하면 API 를 실행하게 되면 에러가 발생 한다.
따라서, ProductSaleslocation, ProductCategory, User, ProductTag entity에 각각 graphql type인
@ObjectType(), @Field 를 추가해야 정상적으로 createProduct API 를 사용할 수 있다.
모든 entity에 다음과 같이 수정해준다.
productSaleslocation.entity.ts 파일 먼저 graphql type 을 추가해보자
// productSaleslocation.entity.ts
import { Field, Float, ObjectType } from '@nestjs/graphql';
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
@ObjectType()
export class ProductSaleslocation {
@PrimaryGeneratedColumn('uuid')
@Field(() => String)
id: string;
@Column()
@Field(() => String)
address: string;
@Column()
@Field(() => String)
addressDetail: string;
@Column({ type: 'decimal', precision: 9, scale: 6 })
@Field(() => Float)
lat: number;
@Column({ type: 'decimal', precision: 9, scale: 6 })
@Field(() => Float)
lng: number;
@Column()
@Field(() => Date)
meetingTime: Date;
}
Float type : 실수 타입으로 지정Date type : 날짜 타입을 지정productTag.entity.ts 파일도 아래와 같이 추가해준다.
// productTag.entity.ts
import { Field, ObjectType } from '@nestjs/graphql';
import { Product } from 'src/apis/products/entities/product.entity';
import { Column, Entity, ManyToMany, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
@ObjectType()
export class ProductTag {
@PrimaryGeneratedColumn('uuid')
@Field(() => String)
id: string;
@Column()
@Field(() => String)
name: string;
@ManyToMany(() => Product, (products) => products.productTags)
@Field(() => [Product])
products: Product[];
}
user.entity.ts 파일도 마찬가지로 추가해준다.
// user.entity.ts
import { Field, ObjectType } from '@nestjs/graphql';
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
@ObjectType()
export class User {
@PrimaryGeneratedColumn('uuid')
@Field(() => String)
id: string;
@Column()
@Field(() => String)
email: string;
@Column()
@Field(() => String)
password: string;
}
이렇게 entity 까지 모두 작성이 끝났다.
이제 만든 createProduct API 를 확인해 보자
yarn start:dev 를 입력해 서버를 실행한다.
🚨 Cannot determine a GraphQL output type 에러가 발생한다면,
Entity 를 제대로 작성하지 않아서 나타나는 에러다. Entity 타입을 제대로 작성해야 한다.

http://localhost:3000/graphql 에 접속해서 플레이그라운드에서 api 요청해보면
🚨 플레이그라운드에서 만든 API 가 나타나지 않는다면,
@Mutation와@Query데코레이터가 빠지지 않았는지 확인해 주자

playground에서 요청 데이터에 isSoldout을 받지 않았지만 false로 저장된 것을 확인할 수 있다.
이전에 entity에서 @Column({ default: false })으로 지정했기 때문이다.
query가 어떻게 날라갔는지 터미널을 통해 확인해 보면

DBeaver도 확인했을 때 데이터가 잘 저장된 것을 확인할 수 있다.

이번에는 데이터를 조회하는 로직을 만들어보자
데이터를 조회하는 fetch 에는 데이터 목록을 가져오는 로직과 데이터 하나씩 가져오는 로직으로 총 2개가 존재한다.
일단 데이터 하나 하나 가져와서 데이터의 상세보기를 하는 로직을 먼저 만들어 보자
products.resolver.ts 파일을 아래와 같이 작성한다.
// products.resolver.ts
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { CreateProductInput } from './dto/create-product.input';
import { Product } from './entities/product.entity';
import { ProductsService } from './products.service';
@Resolver()
export class ProductsResolver {
constructor(
private readonly productsService: ProductsService, //
) {}
@Query(() => Product)
fetchProduct(
@Args('productId') productId: string, //
): Promise<Product> {
return this.productsService.findOne({ productId });
}
@Mutation(() => Product)
createProduct(
@Args('createProductInput') createProductInput: CreateProductInput,
): Promise<Product> {
return this.productsService.create({ createProductInput });
}
}
데이터를 조회할때는 query를 사용했었다.
@Query를 사용해서 반환값을 product를 지정해서 product 엔티티의 객체가 반환된다.
@Query 를 import 할 때에는 '@nestjs/graphql' 에서 import 되어야 한다!!
‘@nestjs/common’ 에서 import 가 된 것은 아닌지 꼭 확인해주자
비즈니스 로직에서 한 개 가져오는 findOne 을 사용했다.
products.service.ts 파일을 아래와 같이 작성해준다.
findOne 메서드는 TypeORM을 함께 사용할 때, 데이터베이스에서 특정 조건을 만족하는 단일 엔티티(Entity)를 조회할 때 사용된다.
주로 Repository나 EntityManager를 통해 사용되며, 주어진 조건에 따라 첫 번째로 일치하는 레코드를 반환한다.
// products.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Product } from './entities/product.entity';
import {
IProductsServiceCreate,
IProductsServiceFindOne,
} from './interfaces/products-service.interface';
@Injectable()
export class ProductsService {
constructor(
@InjectRepository(Product)
private readonly productsRepository: Repository<Product>,
) {}
// 1개씩 fetch
findOne({ productId }: IProductsServiceFindOne): Promise<Product> {
return this.productsRepository.findOne({ where: { id: productId } });
}
create({ createProductInput }: IProductsServiceCreate): Promise<Product> {
const result = this.productsRepository.save({
...createProductInput,
// 하나하나 직접 나열하는 방식
// name: '마우스',
// description: '좋은 마우스',
// price: 3000,
});
return result; // {id: akljdfq-1283aad, name: "마우스", description: "좋은 마우스", price: 3000}
}
}
findOne 메서드를 사용해서 product 테이블에 매개변수로 받은 productId 에 해당하는 상품을 1개 조회하는 비즈니스 로직을 추가한다.{ where: { id: productId } } : where을 통해 조회하고자 하는 조건을 적어주었다. async ~ await : findOne 이 이루어질 때까지 기다려 줘야한다.products-service.interface.ts 파일을 아래와 같이 작성해준다.
// products-service.interface.ts
import { CreateProductInput } from '../dto/create-product.input';
import { UpdateProductInput } from '../dto/update-product.input';
import { Product } from '../entities/product.entity';
export interface IProductsServiceCreate {
createProductInput: CreateProductInput;
}
// 추가
export interface IProductsServiceFindOne {
productId: string;
}
$yarn start:dev 를 입력해 서버를 실행시켜 준다

http://localhost:3000/graphql 에 접속해서 플레이그라운들 실행해 api가 잘 작동하는지 확인해준다.

product id 값을 DBeaver에서 복사해서 productId 값에 넣어서 데이터를 조회한다.
해당하는 데이터가 조회된다면 성공이다.
이번에는 데이터 목록을 조회하는 로직을 만들어 보자
products.resolver.ts 파일을 아래와 같이 작성한다.
// products.resolver.ts
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { CreateProductInput } from './dto/create-product.input';
import { Product } from './entities/product.entity';
import { ProductsService } from './products.service';
@Resolver()
export class ProductsResolver {
constructor(
private readonly productsService: ProductsService, //
) {}
@Query(() => [Product])
fetchProducts(): Promise<Product[]> {
return this.productsService.findAll();
}
@Query(() => Product)
fetchProduct(
@Args('productId') productId: string, //
): Promise<Product> {
return this.productsService.findOne({ productId });
}
@Mutation(() => Product)
createProduct(
@Args('createProductInput') createProductInput: CreateProductInput,
): Promise<Product> {
return this.productsService.create({ createProductInput });
}
}
@Query를 사용해서 반환값을 [product] 를 적용해서 배열 안에 객체의 타입으로 지정했다.findAll 을 사용했다.products.service.ts 파일을 아래와 같이 작성한다.
// products.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Product } from './entities/product.entity';
import {
IProductsServiceCreate,
IProductsServiceFindOne,
} from './interfaces/products-service.interface';
@Injectable()
export class ProductsService {
constructor(
@InjectRepository(Product)
private readonly productsRepository: Repository<Product>,
) {}
findAll(): Promise<Product[]> {
return this.productsRepository.find();
}
findOne({ productId }: IProductsServiceFindOne): Promise<Product> {
return this.productsRepository.findOne({ where: { id: productId } });
}
create({ createProductInput }: IProductsServiceCreate): Promise<Product> {
const result = this.productsRepository.save({
...createProductInput,
// 하나하나 직접 나열하는 방식
// name: '마우스',
// description: '좋은 마우스',
// price: 3000,
});
return result; // {id: akljdfq-1283aad, name: "마우스", description: "좋은 마우스", price: 3000}
}
}
find 메서드를 사용해서 product 테이블의 존재하는 모든 데이터를 조회하는 비즈니스 로직을 추가했다.async ~ await : find 가 이루어질 때까지 기다려 줘야한다.http://localhost:3000/graphql 에 접속해서 플레이그라운들 실행시켜주고

다음과 같이 생성된 데이터 모두가 배열 안에 객체 형태로 데이터 목록이 조회되는 것을 확인 할 수 있다.