API Routes

작성일: 2025. 10. 05최종 수정: 2025. 10. 05. 12시 16분

Next.js에서 서버 사이드 API 엔드포인트를 만들 수 있는 기능이다. pages/api 폴더 안에 파일을 생성하면 자동으로 API 엔드포인트가 된다.

왜 필요한가?

프론트엔드 애플리케이션을 만들 때 종종 서버와 통신할 API가 필요하다. 일반적으로는 별도의 백엔드 서버(Express, Django 등)를 구축하지만, Next.js는 같은 프로젝트 내에서 API를 만들 수 있다.

이렇게 하면 다음과 같은 장점이 있다.

  • 별도의 백엔드 서버 없이 API를 만들 수 있다
  • 같은 코드베이스에서 프론트엔드와 백엔드를 관리할 수 있다
  • 배포가 간단하다 (하나의 프로젝트로 배포)
  • TypeScript 타입을 공유할 수 있다

간단한 API가 필요한 경우나, 서버리스 함수로 충분한 경우에 유용하다.

기본 구조

pages/api 폴더 안에 파일을 만들면 자동으로 API 엔드포인트가 된다.

plain
pages/
└── api/
    ├── hello.ts         → /api/hello
    ├── users.ts         → /api/users
    └── posts/
        └── [id].ts      → /api/posts/:id

파일 이름이 곧 API 경로가 된다. 페이지 라우팅과 동일한 방식이다.

기본 핸들러 작성

API Route는 handler 함수를 default export 해야 한다.

typescript
// pages/api/hello.ts
import type { NextApiRequest, NextApiResponse } from 'next';

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  res.status(200).json({ message: 'Hello from API' });
}

handler 함수는 두 개의 인자를 받는다.

  • req: HTTP 요청 정보를 담은 객체
  • res: HTTP 응답을 보내는 객체

위 코드는 /api/hello로 요청이 오면 { message: 'Hello from API' } JSON을 200 상태 코드와 함께 응답한다.

HTTP 메서드 처리

req.method로 HTTP 메서드를 확인하여 다르게 처리할 수 있다.

typescript
// pages/api/users.ts
import type { NextApiRequest, NextApiResponse } from 'next';

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method === 'GET') {
    // 사용자 목록 조회
    res.status(200).json({ users: ['Alice', 'Bob'] });
  } else if (req.method === 'POST') {
    // 새 사용자 생성
    const { name } = req.body;
    res.status(201).json({ message: `Created user: ${name}` });
  } else {
    // 지원하지 않는 메서드
    res.status(405).json({ message: 'Method not allowed' });
  }
}

일반적인 HTTP 메서드는 다음과 같다.

  • GET: 데이터 조회
  • POST: 데이터 생성
  • PUT: 데이터 전체 수정
  • PATCH: 데이터 부분 수정
  • DELETE: 데이터 삭제

Request 처리

Query 파라미터

URL의 쿼리 파라미터는 req.query로 접근한다.

typescript
// pages/api/search.ts
export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const { q, page } = req.query;

  res.status(200).json({
    query: q,
    page: page || '1'
  });
}

/api/search?q=nextjs&page=2로 요청하면 q"nextjs", page"2"가 된다.

Request Body

POST, PUT 등의 요청에서 본문 데이터는 req.body로 접근한다.

typescript
// pages/api/posts.ts
export default function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method === 'POST') {
    const { title, content } = req.body;

    // 데이터베이스에 저장하는 로직...

    res.status(201).json({
      id: 1,
      title,
      content,
      createdAt: new Date().toISOString()
    });
  } else {
    res.status(405).json({ message: 'Method not allowed' });
  }
}

클라이언트에서 JSON으로 요청을 보내면 Next.js가 자동으로 파싱해준다.

Cookies

쿠키는 req.cookies로 접근한다.

typescript
export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const token = req.cookies.token;

  if (!token) {
    res.status(401).json({ message: 'Unauthorized' });
    return;
  }

  res.status(200).json({ message: 'Authenticated' });
}

Response 처리

상태 코드

res.status()로 HTTP 상태 코드를 설정한다.

typescript
// 성공
res.status(200).json({ success: true });

// 생성됨
res.status(201).json({ id: 1 });

// 잘못된 요청
res.status(400).json({ error: 'Bad request' });

// 인증 필요
res.status(401).json({ error: 'Unauthorized' });

// 권한 없음
res.status(403).json({ error: 'Forbidden' });

// 찾을 수 없음
res.status(404).json({ error: 'Not found' });

// 서버 오류
res.status(500).json({ error: 'Internal server error' });

JSON 응답

res.json()으로 JSON 응답을 보낸다.

typescript
export default function handler(req: NextApiRequest, res: NextApiResponse) {
  res.status(200).json({
    name: 'John Doe',
    age: 30,
    hobbies: ['reading', 'coding']
  });
}

리다이렉트

res.redirect()로 다른 경로로 리다이렉트할 수 있다.

typescript
export default function handler(req: NextApiRequest, res: NextApiResponse) {
  res.redirect(307, '/api/new-endpoint');
}

동적 API 라우트

페이지 라우팅과 마찬가지로 동적 세그먼트를 사용할 수 있다.

단일 동적 세그먼트

typescript
// pages/api/posts/[id].ts
export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const { id } = req.query;

  if (req.method === 'GET') {
    // ID로 포스트 조회
    res.status(200).json({
      id,
      title: `Post ${id}`,
      content: 'Content here'
    });
  } else if (req.method === 'DELETE') {
    // ID로 포스트 삭제
    res.status(200).json({ message: `Deleted post ${id}` });
  }
}

/api/posts/123으로 요청하면 id"123"이 된다.

Catch-all 라우트

typescript
// pages/api/posts/[...slug].ts
export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const { slug } = req.query;

  // slug는 배열로 전달됨
  res.status(200).json({
    path: slug
  });
}
  • /api/posts/aslug = ["a"]
  • /api/posts/a/bslug = ["a", "b"]
  • /api/posts/a/b/cslug = ["a", "b", "c"]

환경 변수 사용

API Route는 서버에서만 실행되므로 환경 변수를 안전하게 사용할 수 있다.

typescript
// pages/api/secret.ts
export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const apiKey = process.env.SECRET_API_KEY;

  // 외부 API 호출 등에 사용
  res.status(200).json({ message: 'Success' });
}

.env.local 파일에 환경 변수를 정의한다.

plain
SECRET_API_KEY=your-secret-key
DATABASE_URL=postgresql://...

환경 변수는 클라이언트 번들에 포함되지 않으므로 민감한 정보를 안전하게 사용할 수 있다.

설정 옵션

API Route의 동작을 커스터마이징할 수 있다.

Body Parser 설정

기본적으로 Next.js는 요청 본문을 자동으로 파싱한다. 이를 비활성화하거나 크기 제한을 설정할 수 있다.

typescript
export const config = {
  api: {
    bodyParser: {
      sizeLimit: '1mb', // 요청 본문 크기 제한
    },
  },
};

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  // 핸들러 로직
}

Body parser를 완전히 비활성화하려면 다음과 같이 한다.

typescript
export const config = {
  api: {
    bodyParser: false,
  },
};

파일 업로드나 스트리밍을 처리할 때 유용하다.

실행 시간 제한

서버리스 환경에서 실행 시간을 제한할 수 있다.

typescript
export const config = {
  api: {
    maxDuration: 5, // 최대 5초
  },
};

주의사항

클라이언트 번들에 포함되지 않는다

API Routes의 코드는 클라이언트 번들에 포함되지 않는다. 서버에서만 실행된다.

따라서 다음을 안전하게 사용할 수 있다.

  • 데이터베이스 쿼리
  • 환경 변수
  • 서버 전용 라이브러리
  • 비밀 키

CORS 처리

다른 도메인에서 API를 호출해야 한다면 CORS 헤더를 설정해야 한다.

typescript
export default function handler(req: NextApiRequest, res: NextApiResponse) {
  // CORS 헤더 설정
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');

  // Preflight 요청 처리
  if (req.method === 'OPTIONS') {
    res.status(200).end();
    return;
  }

  // 실제 로직
  res.status(200).json({ message: 'Hello' });
}

정적 생성과 함께 사용할 수 없다

API Routes는 서버에서 실행되므로 정적 사이트 생성(SSG) 시에는 호출할 수 없다. getStaticProps에서 API Route를 호출하면 안 된다.

대신 데이터를 직접 가져오는 로직을 공유한다.

typescript
// lib/data.ts
export async function getProducts() {
  // 데이터베이스 쿼리
  return products;
}

// pages/api/products.ts
import { getProducts } from '@/lib/data';

export default async function handler(req, res) {
  const products = await getProducts();
  res.status(200).json(products);
}

// pages/index.tsx
import { getProducts } from '@/lib/data';

export async function getStaticProps() {
  const products = await getProducts(); // API Route 호출 X, 직접 호출 O
  return { props: { products } };
}

참고 자료