프론트만 하다가 API Route를 만져봤다
Article
지금까지 API는 항상 "받는 쪽"이었다. 백엔드 개발자가 만들어준 엔드포인트에 fetch를 날리고, 응답을 화면에 뿌리는 것까지가 내 일이었다. 이번에 사이드 프로젝트에 문의 폼을 붙이면서 처음으로 **"만드는 쪽"**에 서봤다. Next.js의 Route Handler 덕분이다.
API Route는 그냥 파일 하나다
거창한 서버 세팅을 상상했는데, 실체는 app 폴더 안에 route.ts 파일 하나 만드는 게 전부였다.
// app/api/contact/route.ts
export async function POST(request: Request) {
const { name, email, message } = await request.json();
if (!email || !message) {
return Response.json({ error: "필수 항목 누락" }, { status: 400 });
}
// 메일 발송 서비스에 전달
await sendMail({ name, email, message });
return Response.json({ ok: true });
}
POST라는 이름의 함수를 export하면 그게 곧 POST 엔드포인트가 된다. Request를 받아서 Response를 돌려준다. 프론트에서 매일 쓰던 fetch의 반대편을 그대로 뒤집어 놓은 모양이라, 낯설 줄 알았던 문법이 오히려 익숙했다. 백엔드가 거창한 게 아니라 프론트의 연장이구나 싶었던 순간이다.
진짜 배운 건 보안 기본기
두 번째로 만든 건 외부 API 프록시였다. 유튜브 데이터를 가져오는 기능인데, 처음엔 클라이언트에서 바로 호출했다.
// ❌ 이러면 API 키가 브라우저에 노출된다
fetch(`https://api.example.com/videos?key=${API_KEY}`);
클라이언트 코드는 전부 사용자에게 배달된다. 개발자 도구 네트워크 탭만 열어도 키가 그대로 보인다. 그래서 서버를 한 번 거치게 바꿨다.
// app/api/videos/route.ts
export async function GET() {
const res = await fetch(
`https://api.example.com/videos?key=${process.env.API_KEY}`,
{ next: { revalidate: 3600 } },
);
if (!res.ok) {
return Response.json([], { status: 200 }); // 실패 시 빈 배열
}
const data = await res.json();
return Response.json(data);
}
클라이언트는 /api/videos만 호출하고, 키는 서버의 환경변수 안에만 존재한다. 토큰과 시크릿은 절대 브라우저로 내려보내지 않는다 — 이 원칙 하나를 몸으로 이해한 게 이번 작업의 가장 큰 수확이다. NEXT_PUBLIC_ 접두사가 붙은 환경변수만 클라이언트에 노출된다는 Next.js의 규칙도 이제야 왜 그렇게 설계됐는지 납득이 된다.
프론트 하던 감각이 그대로 쓰인다
며칠 만져본 소감을 정리하면 이렇다.
- 요청/응답 구조는 이미 알고 있었다. fetch를 쓰는 쪽에서 수백 번 봤던 상태 코드, 헤더, JSON이 그대로 나온다.
- 에러 처리 습관도 그대로 통한다. 응답이 실패하면 빈 배열을 돌려주는 패턴은 프론트에서 하던 방어 코드와 같은 사고방식이다.
- 다만 책임의 무게가 다르다. 클라이언트 버그는 그 사용자 화면에서 끝나지만, 서버에서 키가 새면 전체가 뚫린다.
물론 이 정도로 백엔드를 안다고 말할 수는 없다. DB 설계, 인증, 스케일링 같은 세계는 아직 문 밖이다. 그래도 폼 전송과 프록시 정도는 이제 남의 손을 빌리지 않고 만들 수 있다. 경계선 하나를 넘은 느낌이고, 이 느낌이 꽤 오래갈 것 같다.