Next.js API 에러 처리 깔끔하게 하기 (feat. 커스텀 ApiError 클래스, 커스텀 미들웨어)

허지예·2024년 8월 16일
0

개인 프로젝트 기록

목록 보기
16/17

Next.js 풀스택 개인 프로젝트를 하면서, API를 개발하고 있다.

안정적인 서버를 만들기 위해서 에러 처리에 신경을 쓰고 있다.

이런 저런 고민을 하면서 2가지 정도 도입을 해봤다.

  • 커스텀 ApiError 클래스 작성과 활용
  • 미들웨어 로직을 직접 구현해서, API 별로 미들웨어를 조합해서 사용

1. 커스텀 APIError 클래스 작성과 활용

(1) API 에러 클래스들 선언

  • 쓸 법한 에러 상태들을 아래처럼 클래스로 정의해놨다.
  • 그리고 toRespnose()를 활용해서 NextResponse 형태로 반환할 수 있도록 했다.
import { NextResponse } from 'next/server';

export class ApiError extends Error {
  public readonly statusCode: number;

  constructor(message: string, statusCode?: number) {
    super(message);
    this.name = this.constructor.name;
    this.statusCode = statusCode || 500;
  }

  public toResponse(): NextResponse<{ error: string }> {
    return NextResponse.json({ error: this.message }, { status: this.statusCode });
  }
}

/* 400 Bad Request */
export class BadRequestError extends ApiError {
  constructor(message?: string) {
    super(message ?? 'Bad Request', 400);
  }
}

/* 401 Unauthorized */
export class UnauthorizedError extends ApiError {
  constructor(message?: string) {
    super(message ?? 'Unauthorized', 401);
  }
}

/* 403 Forbidden */
export class ForbiddenError extends ApiError {
  constructor(message?: string) {
    super(message ?? 'Forbidden', 403);
  }
}

/* 404 Not Found */
export class NotFoundError extends ApiError {
  constructor(message?: string) {
    super(message ?? 'Not Found', 404);
  }
}

/* 405 Method Not Allowed */
export class MethodNotAllowedError extends ApiError {
  constructor(message?: string) {
    super(message ?? 'Method Not Allowed', 405);
  }
}

/* 409 Conflict */
export class ConflictError extends ApiError {
  constructor(message?: string) {
    super(message ?? 'Conflict', 409);
  }
}

/* 418 I'm a teapot 😝 */
export class ImATeapotError extends ApiError {
  constructor(message?: string) {
    super(message ?? "I'm a teapot", 418);
  }
}

/* 429 Too Many Requests */
export class TooManyRequestsError extends ApiError {
  constructor(message?: string) {
    super(message ?? 'Too Many Requests', 429);
  }
}

/* 500 Internal Server Error */
export class InternalServerError extends ApiError {
  constructor(message?: string) {
    super(message ?? 'Internal Server Error', 500);
  }
}

/* 501 Not Implemented */
export class NotImplementedError extends ApiError {
  constructor(message?: string) {
    super(message ?? 'Not Implemented', 501);
  }
}

(2) 에러 클래스 사용

  • 요렇게 Response로 만들어서 바로 반환하거나
// some api function
export function GET(request: NextRequest) {
	return new InternalServerError("서버 에러!!").toResponse();
}
  • Error를 상속받아 ApiError 클래스를 만들었으니 아래처럼 throw해서 한번에 처리할 수도 있다.
// some api function
export function GET(request: NextRequest) {
  try {
    // some code...
	throw new InternalServerError("서버 에러!!");
    // ...
  }  catch (error) {
    if (error instanceof ApiError) {
      return error.toResponse();
    }
    console.error('Unexpected error:', error);
    return new InternalServerError('서버에서 알 수 없는 오류가 발생했습니다.').toResponse();
  }
}

2. 미들웨어 직접 구현하기

(1) 미들웨어 로직 구현

  • middleware들을 체이닝해주는 함수 handler를 작성해주었다.
  • Symbol을 반환하는 함수를 next로 넣어주고, 반환값이 Symbol과 같으면 다음 미들웨어를 실행한다!
  • 만약 return next()로 반환되지 않고, NextResponse가 반환되었으면 다음 미들웨어를 실행하지 않는다.
import { NextRequest } from 'next/server';

export * from './middleware.auth';
export * from './middleware.validate';

type NextAPIContext = { params?: Record<string, string>; searchParams?: URLSearchParams }

export function handler(...middleware: Function[]) {
  return async (request: NextRequest, context: NextAPIContext) => {
    let result;

    for (const fn of middleware) {
      const symbol = Symbol('next');

      const next = () => symbol;

      result = await fn(request, context, next);
      if (result !== symbol) {
        break;
      }
    }

    if (result) {
      return result;
    }
    throw new Error('Handler or middleware must return a NextResponse!');
  };
}

(2) 미들웨어 작성 예시

인증 여부를 확인하는 미들웨어

  • 간단하게 수정한 버전
  • 인증 여부를 확인하고 uid를 알아내서 request header에 넣어 다음 미들웨어에서 쉽게 uid를 알아낼 수 있게 했다.
export async function tokenMiddleware(request: NextRequest, context: NextAPIContext, next: () => symbol) {
  const token = request.cookies.get('token')?.value;
  if (!token) {
    return new UnauthorizedError('로그인이 필요합니다.').toResponse();
  }

  try {
    const user = await auth.verifyIdToken(token);
    request.headers.set('x-uid', user.uid);
  } catch (error) {
    return new UnauthorizedError('토큰이 유효하지 않습니다. 다시 로그인해주세요.').toResponse();
  }
  return next();
}

export function getUserId(request: NextRequest) {
  const uid = request.headers.get('x-uid');
  if (!uid) {
    throw new InternalServerError('인증 미들웨어에 문제가 있습니다. 개발자에게 문의해주세요.');
  }
  return uid;
}

(3) 미들웨어 사용 예시

  • 아래처럼 api에 적절한 미들웨어를 조합해서 사용할 수 있다.
  • 나는 request body validate 등을 추가해서 미들웨어를 달아주기도 했다.
function gateway(request: NextRequest) {
  try {
      const userId = getUserId(request);
      
      // 할 일...
    } catch (error) {
      if (error instanceof ApiError) {
        return error.toResponse();
      }
      console.error('Unexpected error:', error);
      return new InternalServerError('서버에서 알 수 없는 오류가 발생했습니다.').toResponse();
    }
}


export const GET = handler(
  tokenMiddleware,
  gateway
);
profile
대학생에서 취준생으로 진화했다가 지금은 풀스택 개발자로 2차 진화함

0개의 댓글

관련 채용 정보