Next.js API 정리
Next.js API 완벽 가이드: API Routes부터 RESTful 설계까지
서론
Next.js는 풀스택 프레임워크로, 프론트엔드와 백엔드 API를 하나의 프로젝트에서 개발할 수 있습니다. API Routes를 통해 서버리스 함수를 만들고, RESTful API를 설계하며, 외부 API와 통신하는 방법을 이해하는 것은 현대 웹 개발에서 필수적입니다. 이 글에서는 Next.js의 API Routes를 중심으로 API 개발의 모든 것을 다루겠습니다.
1. Next.js API Routes 기본
1.1 API Routes란?
Next.js API Routes는 서버 사이드 API 엔드포인트를 만들 수 있게 해주는 기능입니다. pages/api 또는 app/api 디렉토리에 파일을 생성하면 자동으로 API 엔드포인트가 됩니다.
1.2 기본 API Route 생성
// pages/api/hello.js (Pages Router)
export default function handler(req, res) {
res.status(200).json({ message: 'Hello API' });
}
// app/api/hello/route.js (App Router)
export async function GET(request) {
return Response.json({ message: 'Hello API' });
}
1.3 HTTP 메서드 처리
// pages/api/users.js
export default function handler(req, res) {
const { method } = req;
switch (method) {
case 'GET':
res.status(200).json({ users: [] });
break;
case 'POST':
const { name, email } = req.body;
res.status(201).json({ id: 1, name, email });
break;
default:
res.setHeader('Allow', ['GET', 'POST']);
res.status(405).end(`Method ${method} Not Allowed`);
}
}
// app/api/users/route.js (App Router)
export async function GET(request) {
return Response.json({ users: [] });
}
export async function POST(request) {
const body = await request.json();
const { name, email } = body;
return Response.json({ id: 1, name, email }, { status: 201 });
}
2. RESTful API 설계 원칙
2.1 RESTful API 기본 원칙
- 리소스 중심: URL은 리소스를 나타냄
- HTTP 메서드: GET, POST, PUT, DELETE 등으로 동작 정의
- 상태 코드: 적절한 HTTP 상태 코드 사용
- 일관성: 일관된 URL 구조와 응답 형식
2.2 RESTful API 예제
// app/api/posts/route.js
export async function GET(request) {
const { searchParams } = new URL(request.url);
const page = searchParams.get('page') || 1;
const limit = searchParams.get('limit') || 10;
// 데이터베이스에서 포스트 조회
const posts = await getPosts({ page, limit });
return Response.json({
data: posts,
pagination: { page, limit, total: posts.length },
});
}
export async function POST(request) {
const body = await request.json();
const { title, content } = body;
// 유효성 검사
if (!title || !content) {
return Response.json(
{ error: '제목과 내용은 필수입니다.' },
{ status: 400 }
);
}
// 포스트 생성
const post = await createPost({ title, content });
return Response.json(post, { status: 201 });
}
// app/api/posts/[id]/route.js
export async function GET(request, { params }) {
const { id } = params;
const post = await getPostById(id);
if (!post) {
return Response.json({ error: '포스트를 찾을 수 없습니다.' }, { status: 404 });
}
return Response.json(post);
}
export async function PUT(request, { params }) {
const { id } = params;
const body = await request.json();
const post = await updatePost(id, body);
return Response.json(post);
}
export async function DELETE(request, { params }) {
const { id } = params;
await deletePost(id);
return Response.json({ message: '삭제되었습니다.' }, { status: 200 });
}
3. 요청과 응답 처리
3.1 요청 데이터 파싱
// app/api/users/route.js
export async function POST(request) {
// JSON 데이터
const jsonData = await request.json();
// FormData
const formData = await request.formData();
const name = formData.get('name');
// URL 쿼리 파라미터
const { searchParams } = new URL(request.url);
const page = searchParams.get('page');
return Response.json({ success: true });
}
3.2 응답 형식 표준화
// lib/api-response.js
export function successResponse(data, status = 200) {
return Response.json(
{
success: true,
data,
},
{ status }
);
}
export function errorResponse(message, status = 400) {
return Response.json(
{
success: false,
error: message,
},
{ status }
);
}
// 사용 예시
import { successResponse, errorResponse } from '@/lib/api-response';
export async function GET(request) {
try {
const data = await fetchData();
return successResponse(data);
} catch (error) {
return errorResponse(error.message, 500);
}
}
3.3 파일 업로드 처리
// app/api/upload/route.js
export async function POST(request) {
const formData = await request.formData();
const file = formData.get('file');
if (!file) {
return Response.json({ error: '파일이 없습니다.' }, { status: 400 });
}
const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);
// 파일 저장
const filename = `${Date.now()}-${file.name}`;
const filepath = path.join(process.cwd(), 'public', 'uploads', filename);
await writeFile(filepath, buffer);
return Response.json({
url: `/uploads/${filename}`,
});
}
4. 인증과 보안
4.1 JWT 인증
// app/api/auth/login/route.js
import jwt from 'jsonwebtoken';
export async function POST(request) {
const { email, password } = await request.json();
// 사용자 인증
const user = await authenticateUser(email, password);
if (!user) {
return Response.json(
{ error: '이메일 또는 비밀번호가 올바르지 않습니다.' },
{ status: 401 }
);
}
// JWT 토큰 생성
const token = jwt.sign(
{ userId: user.id, email: user.email },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
return Response.json({
token,
user: { id: user.id, email: user.email },
});
}
4.2 미들웨어로 인증 검증
// lib/auth.js
import jwt from 'jsonwebtoken';
export function verifyToken(request) {
const authHeader = request.headers.get('authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return null;
}
const token = authHeader.substring(7);
try {
return jwt.verify(token, process.env.JWT_SECRET);
} catch (error) {
return null;
}
}
// app/api/protected/route.js
import { verifyToken } from '@/lib/auth';
export async function GET(request) {
const user = verifyToken(request);
if (!user) {
return Response.json(
{ error: '인증이 필요합니다.' },
{ status: 401 }
);
}
return Response.json({ message: '인증된 사용자입니다.', user });
}
4.3 CORS 설정
// app/api/cors-example/route.js
export async function GET(request) {
return Response.json(
{ message: 'CORS enabled' },
{
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
}
);
}
5. 데이터베이스 연동
5.1 Prisma와 연동
// app/api/posts/route.js
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export async function GET(request) {
const posts = await prisma.post.findMany({
include: {
author: true,
comments: true,
},
});
return Response.json(posts);
}
export async function POST(request) {
const body = await request.json();
const { title, content, authorId } = body;
const post = await prisma.post.create({
data: {
title,
content,
authorId,
},
include: {
author: true,
},
});
return Response.json(post, { status: 201 });
}
5.2 MongoDB와 연동
// lib/mongodb.js
import { MongoClient } from 'mongodb';
const uri = process.env.MONGODB_URI;
const client = new MongoClient(uri);
export async function connectDB() {
if (!client.topology || !client.topology.isConnected()) {
await client.connect();
}
return client.db('myapp');
}
// app/api/posts/route.js
import { connectDB } from '@/lib/mongodb';
export async function GET(request) {
const db = await connectDB();
const posts = await db.collection('posts').find({}).toArray();
return Response.json(posts);
}
6. 외부 API 호출
6.1 서버 사이드에서 외부 API 호출
// app/api/external/route.js
export async function GET(request) {
try {
const response = await fetch('https://api.example.com/data', {
headers: {
'Authorization': `Bearer ${process.env.API_KEY}`,
},
});
if (!response.ok) {
throw new Error('API 호출 실패');
}
const data = await response.json();
return Response.json(data);
} catch (error) {
return Response.json(
{ error: error.message },
{ status: 500 }
);
}
}
6.2 API 캐싱
// app/api/cached/route.js
export async function GET(request) {
const response = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 }, // 1시간 캐시
});
const data = await response.json();
return Response.json(data, {
headers: {
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
},
});
}
7. 에러 처리
7.1 전역 에러 핸들러
// app/api/posts/route.js
export async function GET(request) {
try {
const posts = await getPosts();
return Response.json(posts);
} catch (error) {
console.error('API Error:', error);
return Response.json(
{
error: '서버 오류가 발생했습니다.',
message: process.env.NODE_ENV === 'development' ? error.message : undefined,
},
{ status: 500 }
);
}
}
7.2 유효성 검사
// lib/validation.js
import { z } from 'zod';
export const postSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(10),
authorId: z.number().int().positive(),
});
// app/api/posts/route.js
import { postSchema } from '@/lib/validation';
export async function POST(request) {
const body = await request.json();
// 유효성 검사
const validationResult = postSchema.safeParse(body);
if (!validationResult.success) {
return Response.json(
{
error: '유효성 검사 실패',
details: validationResult.error.errors,
},
{ status: 400 }
);
}
const post = await createPost(validationResult.data);
return Response.json(post, { status: 201 });
}
8. 실전 예제: 블로그 API
8.1 완전한 블로그 API
// app/api/posts/route.js
import { PrismaClient } from '@prisma/client';
import { postSchema } from '@/lib/validation';
import { verifyToken } from '@/lib/auth';
const prisma = new PrismaClient();
export async function GET(request) {
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '10');
const skip = (page - 1) * limit;
const [posts, total] = await Promise.all([
prisma.post.findMany({
skip,
take: limit,
include: {
author: {
select: { id: true, name: true, email: true },
},
_count: {
select: { comments: true },
},
},
orderBy: { createdAt: 'desc' },
}),
prisma.post.count(),
]);
return Response.json({
data: posts,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
});
}
export async function POST(request) {
const user = verifyToken(request);
if (!user) {
return Response.json(
{ error: '인증이 필요합니다.' },
{ status: 401 }
);
}
const body = await request.json();
const validationResult = postSchema.safeParse(body);
if (!validationResult.success) {
return Response.json(
{
error: '유효성 검사 실패',
details: validationResult.error.errors,
},
{ status: 400 }
);
}
const post = await prisma.post.create({
data: {
...validationResult.data,
authorId: user.userId,
},
include: {
author: {
select: { id: true, name: true, email: true },
},
},
});
return Response.json(post, { status: 201 });
}
9. 성능 최적화
9.1 데이터베이스 쿼리 최적화
// N+1 문제 해결
export async function GET(request) {
// ❌ 나쁜 예: N+1 문제
const posts = await prisma.post.findMany();
for (const post of posts) {
post.author = await prisma.user.findUnique({ where: { id: post.authorId } });
}
// ✅ 좋은 예: include 사용
const posts = await prisma.post.findMany({
include: { author: true },
});
return Response.json(posts);
}
11. 결론
Next.js API Routes는 풀스택 애플리케이션을 구축하는 강력한 도구입니다. RESTful 원칙을 따르고, 적절한 에러 처리와 인증을 구현하며, 데이터베이스와 효율적으로 연동하면 견고한 API를 만들 수 있습니다.
주요 포인트:
- RESTful 설계: 일관된 URL 구조와 HTTP 메서드 사용
- 인증과 보안: JWT를 활용한 인증 구현
- 에러 처리: 명확한 에러 메시지와 상태 코드
- 성능 최적화: 캐싱과 쿼리 최적화
- 유효성 검사: Zod 등을 활용한 데이터 검증
Next.js의 API Routes를 활용하면 별도의 백엔드 서버 없이도 완전한 풀스택 애플리케이션을 구축할 수 있으며, 서버리스 환경에서도 효율적으로 동작합니다.