Nuxt는 이 디렉터리 안의 파일들을 자동으로 스캔하여 HMR(Hot Module Replacement)을 지원하는 API 및 서버 핸들러를 등록합니다.
-| server/
---| api/
-----| hello.ts # /api/hello
---| routes/
-----| bonjour.ts # /bonjour
---| middleware/
-----| log.ts # 모든 요청을 로그
각 파일은 defineEventHandler() 또는 그 별칭인 eventHandler()로 정의된 기본 함수를 export 해야 합니다.
핸들러는 JSON 데이터, Promise를 직접 반환하거나 event.node.res.end()를 사용해 응답을 전송할 수 있습니다.
export default defineEventHandler((event) => {
return {
hello: 'world',
}
})
이제 페이지와 컴포넌트 어디에서나 이 API를 범용적으로 호출할 수 있습니다:
<script setup lang="ts">
const { data } = await useFetch('/api/hello')
</script>
<template>
<pre>{{ data }}</pre>
</template>
~/server/api 내부의 파일들은 라우트에서 자동으로 /api 접두사가 붙습니다.
/api 접두사 없이 서버 라우트를 추가하려면, ~/server/routes 디렉터리에 넣으면 됩니다.
예시:
export default defineEventHandler(() => 'Hello World!')
위 예시에서 /hello 라우트는 http://localhost:3000/hello 에서 접근할 수 있습니다.
Nuxt는 ~/server/middleware 안의 모든 파일을 자동으로 읽어 프로젝트용 서버 미들웨어를 생성합니다.
미들웨어 핸들러는 다른 어떤 서버 라우트보다 먼저 모든 요청에서 실행되어 헤더를 추가하거나 검사하고, 요청을 로그로 남기거나, 이벤트의 요청 객체를 확장합니다.
예시:
export default defineEventHandler((event) => {
console.log('New request: ' + getRequestURL(event))
})
export default defineEventHandler((event) => {
event.context.auth = { user: 123 }
})
Nuxt는 ~/server/plugins 디렉터리 안의 모든 파일을 자동으로 읽어 Nitro 플러그인으로 등록합니다. 이를 통해 Nitro의 런타임 동작을 확장하고 라이프사이클 이벤트에 훅을 걸 수 있습니다.
예시:
export default defineNitroPlugin((nitroApp) => {
console.log('Nitro plugin', nitroApp)
})
서버 라우트는 편리한 헬퍼 세트를 제공하는 h3js/h3에 의해 구동됩니다.
~/server/utils 디렉터리 안에 직접 더 많은 헬퍼를 추가할 수 있습니다.
예를 들어, 원래 핸들러를 감싸고 최종 응답을 반환하기 전에 추가 작업을 수행하는 커스텀 핸들러 유틸리티를 정의할 수 있습니다.
예시:
import type { EventHandler, EventHandlerRequest } from 'h3'
export const defineWrappedResponseHandler = <T extends EventHandlerRequest, D> (
handler: EventHandler<T, D>,
): EventHandler<T, D> =>
defineEventHandler<T>(async (event) => {
try {
// 라우트 핸들러 전에 무언가 수행
const response = await handler(event)
// 라우트 핸들러 후에 무언가 수행
return { response }
} catch (err) {
// 에러 처리
return { err }
}
})
server/ 디렉터리는 app/ 디렉터리와 다른 컨텍스트에서 실행되므로, 자동 임포트 및 기타 타입이 다릅니다.
기본적으로 Nuxt 4는 server/ 폴더를 포함하는 프로젝트 레퍼런스를 가진 tsconfig.json을 생성하여 정확한 타입을 보장합니다.
서버 라우트는 /api/hello/[name].ts처럼 파일 이름에 대괄호를 사용해 동적 파라미터를 정의할 수 있으며, event.context.params를 통해 접근할 수 있습니다.
export default defineEventHandler((event) => {
const name = getRouterParam(event, 'name')
return `Hello, ${name}!`
})
이제 /api/hello/nuxt에서 이 API를 범용적으로 호출하면 Hello, nuxt!를 얻을 수 있습니다.
핸들러 파일 이름에 .get, .post, .put, .delete 등을 접미사로 붙여 요청의 HTTP Method에 매칭할 수 있습니다.
export default defineEventHandler(() => 'Test get handler')
export default defineEventHandler(() => 'Test post handler')
위 예시에서 /test를 요청하면:
Test get handler 반환Test post handler 반환또한 디렉터리 안에서 index.[method].ts를 사용해 코드를 다른 방식으로 구조화할 수 있으며, 이는 API 네임스페이스를 만드는 데 유용합니다.
export default defineEventHandler((event) => {
// `api/foo` 엔드포인트의 GET 요청 처리
})
export default defineEventHandler((event) => {
// `api/foo` 엔드포인트의 POST 요청 처리
})
export default defineEventHandler((event) => {
// `api/foo/bar` 엔드포인트의 GET 요청 처리
})
캐치올(catch-all) 라우트는 폴백(fallback) 라우트 처리를 위해 유용합니다.
예를 들어, ~/server/api/foo/[...].ts라는 파일을 만들면 /api/foo/bar/baz와 같이 어떤 라우트 핸들러와도 매칭되지 않는 모든 요청에 대한 캐치올 라우트가 등록됩니다.
export default defineEventHandler((event) => {
// event.context.path 로 라우트 경로 가져오기: '/api/foo/bar/baz'
// event.context.params._ 로 라우트 세그먼트 가져오기: 'bar/baz'
return `Default foo handler`
})
~/server/api/foo/[...slug].ts처럼 이름을 지정해 캐치올 라우트를 만들 수 있으며, event.context.params.slug를 통해 접근할 수 있습니다.
export default defineEventHandler((event) => {
// event.context.params.slug 로 라우트 세그먼트 가져오기: 'bar/baz'
return `Default foo handler`
})
export default defineEventHandler(async (event) => {
const body = await readBody(event)
return { body }
})
이제 다음과 같이 이 API를 범용적으로 호출할 수 있습니다:
<script setup lang="ts">
async function submit () {
const { body } = await $fetch('/api/submit', {
method: 'post',
body: { test: 123 },
})
}
</script>
POST 메서드 요청과만 매칭하기 위해 파일 이름에 submit.post.ts를 사용하고 있습니다. GET 요청 내에서 readBody를 사용하면, readBody는 405 Method Not Allowed HTTP 에러를 던집니다.예시 쿼리 /api/query?foo=bar&baz=qux
export default defineEventHandler((event) => {
const query = getQuery(event)
return { a: query.foo, b: query.baz }
})
에러가 발생하지 않으면 200 OK 상태 코드가 반환됩니다.
잡히지 않은 모든 에러는 500 Internal Server Error HTTP 에러를 반환합니다.
다른 에러 코드를 반환하려면 createError로 예외를 던지면 됩니다:
export default defineEventHandler((event) => {
const id = Number.parseInt(event.context.params.id) as number
if (!Number.isInteger(id)) {
throw createError({
statusCode: 400,
statusMessage: 'ID should be an integer',
})
}
return 'All good'
})
다른 상태 코드를 반환하려면 setResponseStatus 유틸리티를 사용하세요.
예를 들어, 202 Accepted를 반환하려면
export default defineEventHandler((event) => {
setResponseStatus(event, 202)
})
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig(event)
const repo = await $fetch('https://api.github.com/repos/nuxt/nuxt', {
headers: {
Authorization: `token ${config.githubToken}`,
},
})
return repo
})
export default defineNuxtConfig({
runtimeConfig: {
githubToken: '',
},
})
NUXT_GITHUB_TOKEN='<my-super-token>'
useRuntimeConfig에 event를 인자로 넘기는 것은 선택 사항이지만, 서버 라우트에서 런타임에 환경 변수에 의해 덮어써진 런타임 설정을 가져오기 위해서는 넘겨주는 것이 권장됩니다.export default defineEventHandler((event) => {
const cookies = parseCookies(event)
return { cookies }
})
기본적으로 서버 라우트에서 fetch 요청을 할 때, 들어오는 요청의 헤더나 요청 컨텍스트는 전달되지 않습니다.
서버 라우트에서 fetch 요청을 하면서 요청 컨텍스트와 헤더를 전달하려면 event.$fetch를 사용할 수 있습니다.
export default defineEventHandler((event) => {
return event.$fetch('/api/forwarded')
})
transfer-encoding, connection, keep-alive, upgrade, expect, host, accept서버 요청을 처리할 때, 응답을 클라이언트로 보내는 것을 막지 않고 비동기 작업(예: 캐싱, 로깅 등)을 수행해야 할 수 있습니다. 이때 event.waitUntil을 사용하면 응답을 지연시키지 않고 백그라운드에서 Promise를 기다릴 수 있습니다.
event.waitUntil 메서드는 핸들러가 종료되기 전에 기다릴 Promise를 인자로 받으며, 응답이 전송된 직후 서버가 핸들러를 종료하더라도 작업이 완료되도록 보장합니다. 이는 런타임 프로바이더와 통합되어, 응답 전송 이후 비동기 작업을 처리하기 위한 네이티브 기능을 활용할 수 있게 해줍니다.
const timeConsumingBackgroundTask = async () => {
await new Promise(resolve => setTimeout(resolve, 1000))
}
export default eventHandler((event) => {
// 응답을 막지 않고 백그라운드 작업 예약
event.waitUntil(timeConsumingBackgroundTask())
// 즉시 클라이언트에 응답 전송
return 'done'
})
nuxt.config에서 nitro 키를 사용해 Nitro 설정을 직접 지정할 수 있습니다.
export default defineNuxtConfig({
// https://nitro.build/config
nitro: {},
})
import { createRouter, defineEventHandler, useBase } from 'h3'
const router = createRouter()
router.get('/test', defineEventHandler(() => 'Hello World'))
export default useBase('/api/hello', router.handler)
import fs from 'node:fs'
import { sendStream } from 'h3'
export default defineEventHandler((event) => {
return sendStream(event, fs.createReadStream('/path/to/file'))
})
export default defineEventHandler(async (event) => {
await sendRedirect(event, '/path/redirect/to', 302)
})
export default fromNodeMiddleware((req, res) => {
res.end('Legacy handler')
})
export default fromNodeMiddleware((req, res, next) => {
console.log('Legacy middleware')
next()
})
async이거나 Promise를 반환하는 레거시 미들웨어에서 next() 콜백을 절대 함께 사용하지 마세요.Nitro는 크로스 플랫폼 스토리지 레이어를 제공합니다. 추가 스토리지 마운트 포인트를 설정하려면 nitro.storage 또는 server plugins를 사용할 수 있습니다.
Redis 스토리지를 추가하는 예시:
nitro.storage 사용:
export default defineNuxtConfig({
nitro: {
storage: {
redis: {
driver: 'redis',
/* redis 커넥터 옵션 */
port: 6379, // Redis 포트
host: '127.0.0.1', // Redis 호스트
username: '', // Redis >= 6 필요
password: '',
db: 0, // 기본값은 0
tls: {}, // tls/ssl
},
},
},
})
그런 다음 API 핸들러에서:
export default defineEventHandler(async (event) => {
// 모든 키 나열
const keys = await useStorage('redis').getKeys()
// 키 설정
await useStorage('redis').setItem('foo', 'bar')
// 키 삭제
await useStorage('redis').removeItem('foo')
return {}
})
또는 서버 플러그인과 런타임 설정을 사용해 스토리지 마운트 포인트를 만들 수도 있습니다:
import redisDriver from 'unstorage/drivers/redis'
export default defineNitroPlugin(() => {
const storage = useStorage()
// 런타임 설정 또는 다른 소스에서 자격 증명을 동적으로 전달
const driver = redisDriver({
base: 'redis',
host: useRuntimeConfig().redis.host,
port: useRuntimeConfig().redis.port,
/* 기타 redis 커넥터 옵션 */
})
// 드라이버 마운트
storage.mount('redis', driver)
})
export default defineNuxtConfig({
runtimeConfig: {
redis: { // 기본값
host: '',
port: 0,
/* 기타 redis 커넥터 옵션 */
},
},
})