Nuxt 4
Data Fetching và Server Routes
useFetch, useAsyncData, $fetch, state management, server API routes, Nitro engine — Nuxt 4 mới nhất.
1. Data fetching — useFetch vs useAsyncData vs $fetch
useFetch(url)
High-level
Wrapper convenience cho
useAsyncData + $fetch. Dùng cho 95% case fetch URL.useAsyncData(key, fn)
Generic
Async function bất kỳ với cache key. Linh hoạt hơn — fetch nhiều endpoint, transform...
$fetch(url)
Imperative
Promise raw, không reactive, không SSR-aware. Dùng trong event handler, form submit.
useFetch
<script setup lang="ts">
const { data, pending, error, refresh } = await useFetch("/api/users", {
query: { city: "HCM" },
default: () => [],
transform: (rows: User[]) => rows.map(r => ({ ...r, fullName: `${r.first} ${r.last}` })),
pick: ["id", "name"], // chỉ giữ fields này
watch: [filter], // refetch khi filter đổi
lazy: false, // false = block navigation tới khi xong (default trong setup)
server: true, // run trên server (default)
immediate: true, // gọi luôn (default)
key: "users-hcm", // custom cache key
});
</script>
<template>
<div v-if="pending">Loading...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<ul v-else>
<li v-for="u in data" :key="u.id">{{ u.name }}</li>
</ul>
<button @click="refresh()">Reload</button>
</template>
useAsyncData — linh hoạt hơn
<script setup lang="ts">
// Combine nhiều fetch
const { data } = await useAsyncData("dashboard", async () => {
const [users, orders, stats] = await Promise.all([
$fetch("/api/users"),
$fetch("/api/orders"),
$fetch("/api/stats"),
]);
return { users, orders, stats };
});
</script>
$fetch — imperative
<script setup lang="ts">
async function deleteUser(id: number) {
await $fetch(`/api/users/${id}`, { method: "DELETE" });
await refresh(); // refetch useFetch caller
}
async function createUser(form: UserForm) {
return await $fetch("/api/users", {
method: "POST",
body: form,
});
}
</script>
⭐ Câu PV cốt lõi:"
useFetch vs $fetch khác gì?""
useFetchSSR-aware — chạy 1 lần ở server, kết quả serialize vào HTML, client hydrate KHÔNG chạy lại. Reactive (data, pending, error). Có cache theo key.
$fetchlà Promise thường — chạy mọi nơi gọi, không cache, không SSR-aware. Dùng cho event handler (button click, form submit). Cả 2 dùng ofetch underneath."
2. Nuxt 4 — Data fetching changes
Auto sharing same key
<!-- ComponentA.vue -->
<script setup>
const { data: users } = await useFetch("/api/users", { key: "users-list" });
</script>
<!-- ComponentB.vue -->
<script setup>
const { data: users } = await useFetch("/api/users", { key: "users-list" });
// Cùng key → chia sẻ data + cache, chỉ fetch 1 lần
</script>
Auto cleanup
Component unmount → data cleanup khỏi memory. Trong Nuxt 3 phải tự lo.
Clear on refetch (vs SWR cũ)
// Nuxt 4
const { data, pending } = useFetch("/api/x");
await refresh(); // pending = true, data = null
// → Sau khi xong: data = new value
// Nuxt 3 behavior:
// pending = true, data = OLD value vẫn giữ
Muốn giữ data cũ trong Nuxt 4:
useFetch("/api/x", {
getCachedData(key) {
return useNuxtApp().payload.data[key] ?? useNuxtApp().static.data[key];
}
});
Abort control (4.2+)
const { data } = useFetch("/api/heavy", {
signal: controller.signal,
});
// Component unmount → tự abort request
3. State management
useState — SSR-safe global state
// app/composables/useAuth.ts
export const useAuth = () => useState("auth", () => ({
user: null as User | null,
token: "" as string,
}));
<script setup>
const auth = useAuth();
console.log(auth.value.user); // shared across components, SSR-safe
</script>
useState khác ref ngoài composable?refngoài composable: shared module-level — không SSR-safe (request này thấy state request trước).useState: scope theo request ở SSR, share trong client navigation.
Pinia — store lớn
pnpm add @pinia/nuxt
// nuxt.config.ts
modules: ['@pinia/nuxt']
// app/stores/user.ts
export const useUserStore = defineStore("user", {
state: () => ({ id: 0, name: "" }),
getters: {
isLoggedIn: (s) => s.id > 0,
},
actions: {
async login(email: string, pwd: string) {
const res = await $fetch("/api/login", { method: "POST", body: { email, pwd } });
this.id = res.id;
this.name = res.name;
},
},
});
<script setup>
const userStore = useUserStore();
</script>
4. Server API routes — Nitro
server/api/** tự thành endpoint. Nitro là engine server-side.
REST endpoints
// server/api/users/index.get.ts → GET /api/users
export default defineEventHandler(async (event) => {
const { city } = getQuery(event);
// ... query DB
return await db.users.findMany({ where: { city: String(city) } });
});
// server/api/users/index.post.ts → POST /api/users
export default defineEventHandler(async (event) => {
const body = await readBody(event);
// Validation
if (!body.email) {
throw createError({ statusCode: 400, message: "Email required" });
}
// ... insert
setResponseStatus(event, 201);
return await db.users.create({ data: body });
});
// server/api/users/[id].get.ts → GET /api/users/123
export default defineEventHandler(async (event) => {
const id = Number(getRouterParam(event, "id"));
const user = await db.users.findUnique({ where: { id } });
if (!user) throw createError({ statusCode: 404 });
return user;
});
// server/api/users/[id].delete.ts → DELETE /api/users/123
export default defineEventHandler(async (event) => {
const id = Number(getRouterParam(event, "id"));
await db.users.delete({ where: { id } });
setResponseStatus(event, 204);
});
⭐ Convention Nuxt 4: filename
<route>.<method>.ts → tự match HTTP method. Hoặc <route>.ts thì handler nhận mọi method, tự switch.Helpers thường dùng
| Helper | Tác dụng |
|---|---|
getQuery(event) | Query string params |
getRouterParam(event, "id") | Path param |
readBody(event) | Parse JSON body |
readMultipartFormData(event) | Parse multipart (file upload) |
setResponseStatus(event, 201) | Set status code |
setResponseHeader(event, "X-Custom", "value") | Set header |
getHeader(event, "authorization") | Get header |
getCookie(event, "session") | Get cookie |
setCookie(event, "session", "value", { httpOnly: true }) | Set cookie |
createError({ statusCode, message }) | Throw error có status |
Server middleware
// server/middleware/auth.ts — chạy mọi request
export default defineEventHandler(async (event) => {
const url = getRequestURL(event);
if (url.pathname.startsWith("/api/admin")) {
const token = getHeader(event, "authorization");
if (!isValidAdminToken(token)) {
throw createError({ statusCode: 401, message: "Unauthorized" });
}
}
});
Storage layer (Nitro)
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
storage: {
cache: { driver: "redis", url: process.env.REDIS_URL }
}
}
});
// In handler
export default defineEventHandler(async () => {
const storage = useStorage("cache");
const cached = await storage.getItem("users:list");
if (cached) return cached;
const users = await db.users.findMany();
await storage.setItem("users:list", users, { ttl: 300 });
return users;
});
5. WebSocket / SSE (Server-Sent Events)
// server/api/sse.get.ts
export default defineEventHandler(async (event) => {
const eventStream = createEventStream(event);
setInterval(() => {
eventStream.push({ event: "tick", data: JSON.stringify({ time: Date.now() }) });
}, 1000);
return eventStream.send();
});
<script setup>
const events = useState("events", () => [] as any[]);
onMounted(() => {
const source = new EventSource("/api/sse");
source.addEventListener("tick", (e) => {
events.value.push(JSON.parse(e.data));
});
});
</script>
6. File upload
// server/api/upload.post.ts
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
export default defineEventHandler(async (event) => {
const formData = await readMultipartFormData(event);
if (!formData) throw createError({ statusCode: 400 });
const file = formData.find(f => f.name === "file");
if (!file?.data) throw createError({ statusCode: 400 });
const filename = `${Date.now()}-${file.filename}`;
await writeFile(join("uploads", filename), file.data);
return { url: `/uploads/${filename}` };
});
<script setup>
async function upload(e: Event) {
const file = (e.target as HTMLInputElement).files?.[0];
if (!file) return;
const form = new FormData();
form.append("file", file);
const res = await $fetch("/api/upload", { method: "POST", body: form });
console.log(res.url);
}
</script>
<template>
<input type="file" @change="upload" />
</template>
7. Plugins — chạy khi Nuxt khởi tạo
// app/plugins/analytics.client.ts — chỉ client (suffix .client)
export default defineNuxtPlugin(() => {
// setup analytics
if (import.meta.client) {
window.dataLayer = window.dataLayer || [];
}
});
// app/plugins/dayjs.ts — cả client + server
import dayjs from "dayjs";
export default defineNuxtPlugin(() => ({
provide: {
dayjs: dayjs,
}
}));
// Dùng
const { $dayjs } = useNuxtApp();
$dayjs(new Date()).format("YYYY-MM-DD");
8. Module ecosystem hot
| Module | Tác dụng |
|---|---|
@nuxt/ui | UI library chính chủ (button, input, modal...) |
@pinia/nuxt | Pinia state management |
@nuxt/image | Image optimization (lazy, format, srcset) |
@nuxt/content | Markdown CMS (file-based) |
@nuxtjs/i18n | i18n |
@vueuse/nuxt | VueUse composables auto-import |
@nuxtjs/tailwindcss | TailwindCSS |
@sidebase/nuxt-auth | Auth (NextAuth-style) |
@nuxt/test-utils | Testing helpers |
9. Deploy
Nitro hỗ trợ deploy đa nền tảng:
# Build cho Node
npx nuxt build
# Static (prerender all)
npx nuxt generate
# Vercel — auto detect, không cần config
# Cloudflare Workers
NITRO_PRESET=cloudflare-workers npx nuxt build
# Netlify, Deno Deploy, AWS Lambda, Bun, Edge — đều OK
Output ở .output/:
.output/server/— server bundle (cho SSR).output/public/— static assets (CDN)
10. Performance tips
- ✅ Dùng
useFetchvới key — auto cache + dedupe. - ✅
routeRulesSSG/ISR cho trang ít đổi. - ✅
<NuxtImg>từ@nuxt/image— lazy + format modern. - ✅ Lazy load component nặng:
LazyMyComponent. - ✅ Server-only component:
<ClientOnly>wrap nếu cần. - ✅ Bundle analyzer:
npx nuxt analyze. - ✅ Nuxt 4.2 — async handler extraction tự giảm bundle.
- ✅ Tránh prop drilling sâu → dùng
useStatehoặc Pinia.
Tiếp theo: Câu hỏi phỏng vấn Nuxt 4