Next.js Route Handler 405 오류 해결: GET/POST 파일 위치와 메서드 설정 확인

2026.05.11·수정 2026.09.12·약 17분·작성: 해비·블로그 소개

학습 목표·선수 지식: Route Handler 메서드 불일치를 실제 HTTP 응답으로 확인합니다. 선수 지식: GET·POST·JSON.

이 글에서 정리하는 내용

Next.js Route Handler 405 오류는 API 코드가 없어서가 아니라 요청한 HTTP 메서드와 `app` 디렉터리의 `route.ts`에서 export한 메서드가 맞지 않을 때 생깁니다. 공식 기준으로 Route Handler는 `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS` 같은 named export를 사용하므로 파일 위치, GET/POST export, 요청 URL, form method를 같이 확인해야 합니다.

Route Handler 파일 위치가 맞는데도 배포 후 경로가 다르게 동작한다면 Vercel 404 NOT_FOUND 라우팅 오류 해결도 함께 확인해 API 라우팅과 rewrites 문제를 분리해야 합니다.

실습 경로: 현재 ZIP전체 코드. 먼저 설명에서 지정한 파일만 읽고, 설정·테스트 파일은 필요한 때 확인하세요.

내 증상이 이거면 여기부터 보세요

Route Handler 파일 위치와 HTTP 메서드 매칭 구조

App Router에서는 pages/api가 아니라 app/api/…/route.ts 파일이 API 엔드포인트가 됩니다. 파일은 있는데 405가 난다면 보통 경로는 맞지만 해당 메서드를 처리하는 함수가 없는 상태입니다.

증상 실제 에러 메시지 먼저 볼 위치 바로 해볼 조치 이동할 섹션
GET 요청이 405 405 Method Not Allowed route.ts export export async function GET 추가 핵심 수정 코드
POST 요청이 405 Method Not Allowed form/fetch method POST 함수와 fetch method 일치 핵심 수정 코드
default export 경고 Detected default export route.ts 작성 방식 default 대신 named export 사용 왜 생기는가
URL은 맞는데 계속 404/405 route path mismatch app/api 경로 폴더 구조와 요청 URL 대조 예외 케이스

405를 재현할 때는 브라우저 Network 탭에서 Request URLRequest Method를 기록하고, 응답의 Allow 헤더를 함께 봅니다. 이 세 값만 있으면 잘못된 요청 경로인지, route.ts에 해당 named export가 빠진 것인지 빠르게 구분할 수 있습니다.

405 Method Not Allowed
No HTTP methods exported in route.ts. Export a named export for each HTTP method.
Detected default export in route.ts. Export a named export for each HTTP method instead.

먼저 적용할 핵심 수정 코드

원인 설명을 오래 읽기 전에 아래 설정부터 현재 코드와 대조해보세요. route.ts에서 요청에 맞는 GET 또는 POST 함수를 named export로 내보내고, 클라이언트 요청 URL과 메서드를 같이 맞춥니다. 중요한 것은 오류를 덮는 옵션을 추가하는 것이 아니라, 실행 환경과 설정 파일이 같은 기준으로 동작하게 만드는 것입니다.

GET/POST Route Handler 기본형

route.ts 전체 코드에서 GET·POST 응답과 JSON 오류 처리를 함께 확인합니다.

Route Handler는 default export가 아니라 HTTP 메서드 이름과 같은 named export를 사용합니다. `OPTIONS`를 직접 정의하지 않으면 Next.js가 정의된 메서드를 기준으로 `Allow` 헤더를 포함한 `OPTIONS` 응답을 자동 처리할 수 있지만, 실제 요청 메서드가 export되어 있지 않으면 405로 이어질 수 있습니다.

클라이언트 요청 메서드 확인

await fetch("/api/contact", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ email }),
})

클라이언트가 POST로 보내는데 route.ts에는 GET만 있으면 경로가 맞아도 405가 납니다.

왜 이런 오류가 생기는가

405는 경로가 없다는 뜻이 아니라 경로는 찾았지만 해당 메서드를 허용하지 않는다는 뜻입니다. Next.js 공식 문서도 지원하지 않는 메서드가 호출되면 `405 Method Not Allowed` 응답을 반환한다고 설명합니다. 그래서 404보다 Route Handler 내부 export와 실제 요청 메서드를 먼저 봐야 합니다.

App Router의 API 파일은 app/api/contact/route.ts처럼 route.ts 이름을 써야 합니다. app/api/contact.ts처럼 pages router 방식과 섞으면 기대한 URL이 만들어지지 않습니다. route.ts 전체 코드

GET 함수와 POST 함수는 같은 route.ts 안에 함께 둘 수 있습니다. 다만 클라이언트 fetch, form method, 서버 export가 같은 메서드 기준으로 맞아야 합니다.

실제 작업에서 점검하는 순서

먼저 로컬에서 실제 요청 URL이 app/api/.../route.ts 폴더 구조와 맞는지 확인합니다. 그다음 GET·POST export와 호출 쪽 fetch 또는 form method를 한 쌍씩 대조하고, 마지막으로 배포 환경에서도 같은 요청을 재현합니다.

두 번째로 한 번에 여러 설정을 바꾸지 않습니다. Next.js Route Handler 405 오류를 해결하다 보면 관련 파일을 전부 고치고 싶어지지만, 그러면 어떤 변경이 실제 해결책이었는지 알기 어렵습니다. 핵심 설정 하나를 바꾸고 검증 명령을 실행한 뒤 다음 설정으로 넘어가야 합니다.

수정 사항은 호출 코드와 route.ts에 함께 남겨야 합니다. 메서드 이름, 요청 본문 형식, 응답 상태 코드를 코드 리뷰에서 바로 확인할 수 있게 하면 다른 개발자도 동일한 405 상황을 재현하고 검증할 수 있습니다.

그래도 안 될 때 볼 예외 케이스

메서드를 추가했는데도 405가 유지되면 개발 서버를 다시 시작하고 빌드 산출물을 새로 만든 뒤, 같은 URL에 이전 함수가 배포된 것은 아닌지 확인합니다. 경로 대소문자와 route group 위치도 로컬·배포 환경에서 다르게 해석될 수 있으므로 함께 대조합니다.

다음에 같은 문제를 줄이는 체크리스트

Next.js API route 405 오류 해결 체크리스트

Route Handler 오류는 파일 위치와 메서드 이름을 함께 봐야 빠르게 잡힙니다. 특히 GET으로 테스트하고 POST만 만든 API를 고장으로 오해하는 경우가 많으니 요청 방법부터 분리하는 습관이 필요합니다.

결국 Next.js Route Handler 405 오류는 한 줄짜리 우회 코드보다 확인 순서가 중요합니다. 에러 문구를 단계별로 나누고, 설정 파일과 실행 명령을 같은 기준으로 맞추면 같은 문제를 훨씬 짧게 끝낼 수 있습니다. 공식 기준은 Route Handlers, route.js 파일 규칙, NextResponse, Route Handler에서 `NextResponse.next()`를 쓰면 안 되는 오류 문서에서 확인했습니다.

GET·POST·OPTIONS·405를 같은 URL로 비교합니다

터미널 · 실행 명령

curl -i http://localhost:3000/api/contact
curl -i -X POST -H "Content-Type: application/json" --data '{"email":"reader@example.com"}' http://localhost:3000/api/contact
curl -i -X DELETE http://localhost:3000/api/contact
curl -i -X OPTIONS http://localhost:3000/api/contact

GET은 200 JSON, 유효 POST는 저장 없이 입력을 돌려주는 201, DELETE는 미구현 메서드의 405입니다. OPTIONS는 프레임워크 자동 응답의 Allow를 확인합니다. 405 응답에 Allow가 자동으로 들어온다고 가정하지 마세요. HTTP 규격이 요구하는 Allow 헤더와 설치된 구현의 실제 응답을 구분합니다. OPTIONS의 Allow는 CORS 허용 헤더가 아니므로 다른 출처의 브라우저 요청 허용은 별도 설계입니다.

현재 실습 ZIP

현재 본문과 같은 실습 파일 내려받기

전체 실행 파일

아래는 함께 실행하는 단일 완성 프로젝트입니다. ZIP을 풀고 프로젝트 폴더에서 npm install, npm run build, npm run start를 실행합니다. 개발은 npm run dev, 타입 검사는 npm run typecheck입니다. Node.js 20.9 이상, 검증 환경 Node.js 24.19.0 / Next.js 16.3.5 / React 19.3.0 / TypeScript 7.0.2입니다.

README.md

# 실습 4597

Node.js 20.9 이상 (검증: 24.19.0). 이 폴더에서 npm install, npm run build, npm run start. 개발은 npm run dev. 타입 검사는 npm run typecheck.
Next.js 16.3.5 / React 19.3.0 / TypeScript 7.0.2. 외부 DB·인증 서비스는 제공하지 않습니다.
검증: 로컬 production build와 HTTP 검사. 브라우저 조작과 실제 배포는 별도 확인합니다.

app/api/contact/route.ts

export async function GET() {
  return Response.json({ ok: true });
}
export async function POST(request: Request) {
  let body: unknown;
  try {
    body = await request.json();
  } catch {
    return Response.json({ error: "JSON 형식이 아닙니다." }, { status: 400 });
  }
  if (
    typeof body !== "object" ||
    body === null ||
    !("email" in body) ||
    typeof body.email !== "string"
  )
    return Response.json(
      { error: "email 문자열이 필요합니다." },
      { status: 422 },
    );
  return Response.json({ received: body.email }, { status: 201 });
}

app/layout.tsx

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ko">
      <body>{children}</body>
    </html>
  );
}

app/page.tsx

export default function Page() {
  return <main>API 실습: /api/contact</main>;
}

next-env.d.ts

/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/types/root-params.d.ts";

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

package.json

{
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build --webpack",
    "start": "next start",
    "typecheck": "next typegen && tsc --noEmit"
  },
  "engines": {
    "node": ">=20.9.0"
  },
  "dependencies": {
    "next": "16.3.5",
    "react": "19.3.0",
    "react-dom": "19.3.0"
  },
  "devDependencies": {
    "typescript": "7.0.2",
    "@types/react": "19.3.0",
    "@types/react-dom": "19.3.0",
    "@types/node": "22.20.2"
  }
}

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2017",
    "lib": ["dom", "esnext"],
    "strict": true,
    "skipLibCheck": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "react-jsx",
    "plugins": [
      {
        "name": "next"
      }
    ],
    "allowJs": true,
    "incremental": true
  },
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts",
    ".next/dev/types/**/*.ts"
  ],
  "exclude": ["node_modules"]
}

실제 검증: next build exit 0; HTTP GET200/POST201/JSON400/입력422/DELETE405/OPTIONS204와 Allow 검사. CORS·외부배포 미검증. 코드 뷰어와 ZIP의 모든 파일을 바이트/복사 텍스트로 대조했습니다.

공식 자료 재확인: 2026-09-12. 이 글의 실행하지 않은 브라우저/배포 점검은 독자 확인 과제로 구분합니다.

공식 기준과 확인 범위

확인일: 2026-09-12. API 설명은 Next.js 16 App Router 기준이며 과거 릴리스 글은 본문의 태그 범위를 유지합니다. 부분 API 코드는 기존 프로젝트에 통합하는 예시입니다.

이 글이 도움이 되었나요?

조회 중

Next.js 학습 순서

필수 13개 · 전체 27개

읽음 기록 관리

전체 과정 목차 (27개)
  1. 필수 길잡이 · Next.js App Router 학습 순서: 설치부터 배포까지
  2. 필수 학습 · Next.js package.json: scripts dependencies 이해
  3. 필수 학습 · Next.js App Router로 만드는 첫 프로젝트 완성 실습 가이드
  4. 필수 학습 · Next.js에서 .next 폴더는 어떤 역할을 할까?
  5. 필수 학습 · Next.js 동적 라우트 완전 정리: [slug], params, catch-all
  6. 선택 참고 · Next.js params should be awaited 해결: App Router 기준
  7. 선택 참고 · Next.js window is not defined 오류 해결: 브라우저 API를 안전하게 쓰기
  8. 선택 참고 · Next.js hydration failed 오류 해결: 원인과 해결 방법
  9. 필수 학습 · Axios 사용법: React Next.js에서 API 요청 구조 잡는 법
  10. 선택 참고 · Next.js Route Handler 405 오류 해결: GET/POST 파일 위치와 메서드 설정 확인 현재 글
  11. 필수 학습 · Next.js Server Actions + React Hook Form 검증 기준
  12. 선택 참고 · Next.js useSearchParams Suspense 오류 해결: 빌드 실패 기준
  13. 선택 참고 · Next.js fetch 캐시 문제 해결: 데이터가 바뀌었는데 화면이 그대로일 때
  14. 선택 참고 · Next.js Dynamic server usage 오류 해결: cookies headers 기준
  15. 선택 참고 · Next.js 환경변수 적용 오류 해결: .env.local을 바꿨는데 값이 안 바뀔 때
  16. 필수 학습 · Next.js Metadata API 완전 정리: 정적 metadata와 generateMetadata
  17. 선택 참고 · Next.js metadata가 적용되지 않을 때 확인할 7가지
  18. 선택 참고 · Next.js Image remotePatterns 오류 해결: 외부 이미지 도메인 허용하기
  19. 필수 학습 · Next.js redirects 설정: next.config.js에서 URL 이동 처리
  20. 필수 학습 · Next.js SEO 체크리스트: metadata·초기 HTML·OG 이미지 점검
  21. 선택 참고 · Next.js SEO 완전 가이드: App Router metadata부터 배포 확인까지
  22. 필수 학습 · Next.js 16 성능 최적화 체크리스트: 번들·이미지·캐시·배포
  23. 필수 학습 · Next.js 렌더링 성능 최적화: React 화면이 느릴 때 기준
  24. 필수 학습 · Next.js 16 Proxy 마이그레이션: Node.js Runtime·matcher 검증
  25. 선택 참고 · Next.js 보안 패치 기준: v16.2.5 영향 범위 점검
  26. 선택 참고 · Next.js SEO SSR 적용법: 검색 노출과 렌더링 구조 잡기
  27. 시점·기록 · Next.js 16.3.0-canary.106의 useCache deprecation 경고와 hybrid not-found 수정 이해하기

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기