본문으로 건너뛰기
개발 머꼬
개발 노트Nuxt.js
hohyeon.dev17

server/api와 server/routes와 server/middleware가 각각 하는 일

  • #Engineering Note
  • #Nuxt.js

문제 발생

프런트에서 외부 API를 직접 부르다가 CORS에 막혔고, 키까지 노출되는 걸 알고 서버 쪽으로 옮기려 했는데 파일을 어디에 둬야 할지, 메서드는 어떻게 나누는지 매번 헷갈렸습니다.

server/api/posts.ts      → ?
server/routes/posts.ts   → ?
server/middleware/*.ts   → ?

원인 분석

server/ 아래 세 디렉터리는 역할이 다릅니다.

  • server/api — 이 안의 파일은 자동으로 /api 접두사가 붙습니다. hello.ts/api/hello가 됩니다.
  • server/routes — 접두사 없이 매핑됩니다. hello.ts/hello입니다. 웹훅 수신처럼 경로가 고정된 엔드포인트에 씁니다.
  • server/middleware — 다른 라우트보다 먼저, 모든 요청에서 실행됩니다.

모든 파일은 기본 export로 defineEventHandler()(또는 별칭 eventHandler())를 내보내야 합니다.

미들웨어에는 제약이 하나 있습니다. 문서가 명시합니다 — 미들웨어 핸들러는 아무것도 반환해서는 안 되며(요청을 닫거나 응답해서도 안 됩니다) 요청 컨텍스트를 검사하거나 확장하거나, 오류를 던지기만 해야 합니다. 여기서 값을 반환하면 그 요청이 그대로 끝나 버립니다.

해결 방안

  1. 파일 이름이 곧 라우트입니다.
server/api/posts/index.get.ts   → GET  /api/posts
server/api/posts/index.post.ts  → POST /api/posts
server/api/posts/[id].get.ts    → GET  /api/posts/:id
server/api/files/[...path].ts   → /api/files/**

.get/.post/.put/.delete 접미사가 HTTP 메서드를 매칭합니다. 접미사가 없으면 모든 메서드를 받습니다.

  1. 요청 데이터는 전용 헬퍼로 읽습니다.
// server/api/posts/index.post.ts
export default defineEventHandler(async (event) => {
  const body = await readBody(event);
  const { page } = getQuery(event);
  return await createPost(body);
});
// server/api/posts/[id].get.ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, "id");
  return await findPost(id);
});
  1. 입력은 경계에서 검증합니다. readBody/getQuery가 주는 값은 전부 외부 입력입니다. Zod 같은 스키마 검증기와 함께 쓰는 형태가 문서에도 안내되어 있습니다.
const body = await readValidatedBody(event, createPostSchema.parse);
  1. 미들웨어에서는 반환하지 않습니다.
// server/middleware/auth.ts
export default defineEventHandler(async (event) => {
  const token = getCookie(event, "session");
  event.context.user = await resolveUser(token);   // 컨텍스트 확장만
  // return 하지 않는다
});

거부해야 하면 오류를 던집니다.

throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
  1. 미들웨어는 정말 모든 요청에서 돕니다. 정적 자산과 무관한 경로까지 포함하므로 무거운 작업을 넣지 않고, 필요한 경로에서만 동작하도록 조건을 겁니다.

  2. 프런트에서는 useFetch로 부릅니다. 같은 서버라 상대 경로면 충분하고, 서버 렌더 시에는 HTTP를 타지 않고 내부에서 직접 처리됩니다.

const { data } = await useFetch("/api/posts");

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.