GraphQL은 Facebook에서 개발한 데이터 쿼리 언어로, RESTful API의 한계를 극복하고 클라이언트가 요청한 데이터만 받아올 수 있게 해준다. 주요 특징은 다음과 같다:
NestJS와 GraphQL을 사용하여 기본 CRUD API를 구현하는 방법을 예시 코드로 보여준다.
import { ObjectType, Field, Int } from '@nestjs/graphql';
@ObjectType()
export class User {
@Field(() => Int)
id: number;
@Field(() => String)
name: string;
@Field(() => String, { nullable: true })
email?: string;
@Field(() => Int)
status: number;
}
import { Args, Int, Mutation, Query, Resolver } from '@nestjs/graphql';
import { UsersService } from './users.service';
import { NewUserInput } from './dto/new-user.input';
import { User } from './models/user.model';
import { UpdateUserInput } from './dto/update-user.input';
import { UsersArgs } from './dto/users.args';
@Resolver()
export class UsersResolver {
constructor(private readonly service: UsersService) {}
@Query(() => User, { nullable: true })
async user(
@Args('id', { type: () => Int }) id: number,
@Args('name', { type: () => String, nullable: true }) name?: string
): Promise<User | null> {
const result = this.service.getUserByIdOrName(id, name);
return result;
}
@Query(() => [User])
users(@Args() usersArgs: UsersArgs): Promise<User[]> {
return this.service.getUsers(usersArgs);
}
@Mutation(() => User)
async addUser(
@Args('newUserData', { type: () => NewUserInput }) newUserData: NewUserInput
): Promise<User> {
const user = await this.service.addUser(newUserData);
return user;
}
@Mutation(() => User)
async updateUser(
@Args('updateUserData', { type: () => UpdateUserInput })
updateUserData: UpdateUserInput
): Promise<User> {
const user = await this.service.updateUser(updateUserData);
return user;
}
@Mutation(() => Boolean)
async deleteUser(
@Args('id', { type: () => Int }) id: number
): Promise<boolean> {
return this.service.deleteUser(id);
}
}
import {
ConflictException,
Injectable,
InternalServerErrorException,
NotFoundException,
} from '@nestjs/common';
import { UsersRepository } from './users.repository';
import { User } from './models/user.model';
import { NewUserInput } from './dto/new-user.input';
import { UpdateUserInput } from './dto/update-user.input';
import { UsersArgs } from './dto/users.args';
@Injectable()
export class UsersService {
constructor(private readonly repository: UsersRepository) {}
async getUserByIdOrName(id: number, name?: string): Promise<User | null> {
const user = await this.repository.findUserByIdOrName(id, name);
if (!user) {
return null;
}
return user;
}
async getUsers(usersArgs: UsersArgs): Promise<User[]> {
const { skip, take } = usersArgs;
const users = await this.repository.findUsers(skip, take);
return users;
}
async addUser(data: NewUserInput): Promise<User> {
try {
const user = await this.repository.create(data);
return user;
} catch (err) {
if (err.name === 'QueryFailedError') {
if (err.code === 23505) {
throw new ConflictException(err);
}
}
throw new InternalServerErrorException(err);
}
}
async updateUser(data: UpdateUserInput): Promise<User> {
const { id, name, email } = data;
const existingUser = await this.repository.findUserByIdOrName(id);
if (!existingUser) {
throw new NotFoundException(`User with ID ${id} not found`);
}
if (name) existingUser.name = name;
if (email) existingUser.email = email;
const updatedUser = await this.repository.updateUser(existingUser);
return updatedUser;
}
async deleteUser(id: number): Promise<boolean> {
const existingUser = await this.repository.findUserByIdOrName(id);
if (!existingUser) {
throw new NotFoundException(`User with ID ${id} not found`);
}
await this.repository.deleteUser(id);
return true;
}
}
import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { User } from './models/user.model';
import { NewUserInput } from './dto/new-user.input';
@Injectable()
export class UsersRepository {
constructor(private readonly dataSource: DataSource) {}
async findUserByIdOrName(id: number, name?: string): Promise<User> {
let condition = '';
let bindNum = 0;
const params = [];
if (name) {
params.push(name);
condition = `AND name = $${++bindNum}`;
}
params.push(id);
let sql = `SELECT id, name, email, status FROM users WHERE id = $${++bindNum} ${condition}`;
const [result] = await this.dataSource.query(sql, params);
return result;
}
async findUsers(skip: number, take: number): Promise<User[]> {
const result = await this.dataSource.query(
`SELECT id, name, email, status FROM users ORDER BY id LIMIT $1 OFFSET $2`,
[take, skip]
);
return result;
}
async create(data: NewUserInput): Promise<User> {
const [result] = await this.dataSource.query(
`INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email, status`,
[data.name, data.email]
);
return result;
}
async updateUser(user: User): Promise<User> {
const { id, name, email } = user;
await this.dataSource.query(
'UPDATE users SET name = $1, email = $2 WHERE id = $3 RETURNING *',
[name, email, id]
);
return user;
}
async deleteUser(id: number): Promise<void> {
await this.dataSource.query('DELETE FROM users WHERE id = $1', [id]);
}
}
스칼라 타입은 GraphQL의 기본 데이터 타입이다. 예를 들어, 커스텀 스칼라 타입을 정의하여 특정 형식을 강제할 수 있다.
import { Scalar, CustomScalar } from '@nestjs/graphql';
import { GraphQLScalarType, Kind } from 'graphql';
@Scalar('Date')
export class DateScalar implements CustomScalar<number, Date> {
description = 'Date custom scalar type';
// 구현 생략...
}
디렉티브는 스키마나 쿼리에서 특정 조건을 적용할 때 사용된다.
directive @upper on FIELD_DEFINITION
type User {
name: String @upper
}
인터페이스는 공통 필드를 가진 타입을 정의한다.
@InterfaceType()
abstract class Animal {
@Field()
name: string;
}
유니언은 여러 타입 중 하나를 반환하고, 열거형은 특정 값 집합을 정의한다.
const AnimalUnion = UnionType('AnimalUnion', () => [Cat, Dog]);
enum Role {
ADMIN = 'ADMIN',
USER = 'USER'
}
필드 미들웨어는 특정 필드에 로직을 적용한다.
const loggingMiddleware: FieldMiddleware = async (ctx, next) => {
console.log('Accessing field:', ctx.info.fieldName);
return next();
};
Mapped Types는 기존 타입을 기반으로 새로운 타입을 생성한다.
@ObjectType()
class UpdateUserInput extends PartialType(User) {}
플러그인은 GraphQL 서버의 기능을 확장한다.
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [ApolloServerPluginLandingPageGraphQLPlayground()],
});
쿼리의 복잡도를 측정하고 제한을 설정한다.
@Complexity(1, (childrenComplexity, args) => childrenComplexity + 10)
여러 GraphQL 서비스 간에 스키마를 결합한다.
import { buildFederatedSchema } from '@apollo/federation';
const schema = buildFederatedSchema([{ typeDefs, resolvers }]);
GraphQL과 NestJS를 사용하여 API를 설계하는 데 필요한 기본 개념과 CRUD 코드 예제를 통해 GraphQL의 강력한 기능을 활용할 수 있는 방법을 이해할 수 있다. 또한, 고급 기능들을 통해 더욱 복잡한 애플리케이션의 요구사항을 만족시킬 수 있다.