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ì?"

"useFetch SSR-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.

$fetch là 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?
  • ref ngoà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

HelperTá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

ModuleTác dụng
@nuxt/uiUI library chính chủ (button, input, modal...)
@pinia/nuxtPinia state management
@nuxt/imageImage optimization (lazy, format, srcset)
@nuxt/contentMarkdown CMS (file-based)
@nuxtjs/i18ni18n
@vueuse/nuxtVueUse composables auto-import
@nuxtjs/tailwindcssTailwindCSS
@sidebase/nuxt-authAuth (NextAuth-style)
@nuxt/test-utilsTesting 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

  1. ✅ Dùng useFetch với key — auto cache + dedupe.
  2. routeRules SSG/ISR cho trang ít đổi.
  3. <NuxtImg> từ @nuxt/image — lazy + format modern.
  4. ✅ Lazy load component nặng: LazyMyComponent.
  5. ✅ Server-only component: <ClientOnly> wrap nếu cần.
  6. ✅ Bundle analyzer: npx nuxt analyze.
  7. ✅ Nuxt 4.2 — async handler extraction tự giảm bundle.
  8. ✅ Tránh prop drilling sâu → dùng useState hoặc Pinia.

© 2026 .NET Fresher Guide. All rights reserved.

Về Trang Web

Hướng dẫn toàn diện để chuẩn bị phỏng vấn vị trí .NET Fresher với nội dung từ lý thuyết đến thực hành.