API Routes
Next.js에서 서버 사이드 API 엔드포인트를 만들 수 있는 기능이다. pages/api 폴더 안에 파일을 생성하면 자동으로 API 엔드포인트가 된다.
왜 필요한가?
프론트엔드 애플리케이션을 만들 때 종종 서버와 통신할 API가 필요하다. 일반적으로는 별도의 백엔드 서버(Express, Django 등)를 구축하지만, Next.js는 같은 프로젝트 내에서 API를 만들 수 있다.
이렇게 하면 다음과 같은 장점이 있다.
- 별도의 백엔드 서버 없이 API를 만들 수 있다
- 같은 코드베이스에서 프론트엔드와 백엔드를 관리할 수 있다
- 배포가 간단하다 (하나의 프로젝트로 배포)
- TypeScript 타입을 공유할 수 있다
간단한 API가 필요한 경우나, 서버리스 함수로 충분한 경우에 유용하다.
기본 구조
pages/api 폴더 안에 파일을 만들면 자동으로 API 엔드포인트가 된다.
pages/
└── api/
├── hello.ts → /api/hello
├── users.ts → /api/users
└── posts/
└── [id].ts → /api/posts/:id
파일 이름이 곧 API 경로가 된다. 페이지 라우팅과 동일한 방식이다.
기본 핸들러 작성
API Route는 handler 함수를 default export 해야 한다.
// 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 메서드를 확인하여 다르게 처리할 수 있다.
// 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로 접근한다.
// 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로 접근한다.
// 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로 접근한다.
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 상태 코드를 설정한다.
// 성공
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 응답을 보낸다.
export default function handler(req: NextApiRequest, res: NextApiResponse) {
res.status(200).json({
name: 'John Doe',
age: 30,
hobbies: ['reading', 'coding']
});
}
리다이렉트
res.redirect()로 다른 경로로 리다이렉트할 수 있다.
export default function handler(req: NextApiRequest, res: NextApiResponse) {
res.redirect(307, '/api/new-endpoint');
}
동적 API 라우트
페이지 라우팅과 마찬가지로 동적 세그먼트를 사용할 수 있다.
단일 동적 세그먼트
// 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 라우트
// 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/a→slug = ["a"]/api/posts/a/b→slug = ["a", "b"]/api/posts/a/b/c→slug = ["a", "b", "c"]
환경 변수 사용
API Route는 서버에서만 실행되므로 환경 변수를 안전하게 사용할 수 있다.
// 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 파일에 환경 변수를 정의한다.
SECRET_API_KEY=your-secret-key
DATABASE_URL=postgresql://...
환경 변수는 클라이언트 번들에 포함되지 않으므로 민감한 정보를 안전하게 사용할 수 있다.
설정 옵션
API Route의 동작을 커스터마이징할 수 있다.
Body Parser 설정
기본적으로 Next.js는 요청 본문을 자동으로 파싱한다. 이를 비활성화하거나 크기 제한을 설정할 수 있다.
export const config = {
api: {
bodyParser: {
sizeLimit: '1mb', // 요청 본문 크기 제한
},
},
};
export default function handler(req: NextApiRequest, res: NextApiResponse) {
// 핸들러 로직
}
Body parser를 완전히 비활성화하려면 다음과 같이 한다.
export const config = {
api: {
bodyParser: false,
},
};
파일 업로드나 스트리밍을 처리할 때 유용하다.
실행 시간 제한
서버리스 환경에서 실행 시간을 제한할 수 있다.
export const config = {
api: {
maxDuration: 5, // 최대 5초
},
};
주의사항
클라이언트 번들에 포함되지 않는다
API Routes의 코드는 클라이언트 번들에 포함되지 않는다. 서버에서만 실행된다.
따라서 다음을 안전하게 사용할 수 있다.
- 데이터베이스 쿼리
- 환경 변수
- 서버 전용 라이브러리
- 비밀 키
CORS 처리
다른 도메인에서 API를 호출해야 한다면 CORS 헤더를 설정해야 한다.
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를 호출하면 안 된다.
대신 데이터를 직접 가져오는 로직을 공유한다.
// 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 } };
}