Nuxt 4 Trong Năm 2026: Cấu Trúc Thư Mục app/ Mới Và Hướng Dẫn Di Chuyển Từ Nuxt 3

Hướng dẫn đầy đủ di chuyển từ Nuxt 3 sang Nuxt 4 với cấu trúc thư mục app/ mới, singleton data fetching, shallow reactivity mặc định, và phân tách ngữ cảnh TypeScript.

Nuxt 4 directory structure migration guide

Nuxt 4 đánh dấu bước tiến hóa quan trọng của framework Vue dành cho phát triển ứng dụng full-stack. Được phát hành vào tháng 7 năm 2025 và hiện tại đã đạt phiên bản 4.4, bản nâng cấp này tập trung vào việc cải thiện cấu trúc dự án, tối ưu hiệu suất data fetching, và tăng cường type safety thông qua phân tách ngữ cảnh TypeScript. Khác với bước nhảy lớn từ Nuxt 2 sang Nuxt 3, quá trình di chuyển lần này được thiết kế để diễn ra từng bước một, cho phép các dự án production chuyển đổi mà không cần viết lại toàn bộ mã nguồn.

Bài viết này trình bày chi tiết các thay đổi cốt lõi trong Nuxt 4 — từ cấu trúc thư mục app/ mới đến cơ chế singleton data fetching, shallow reactivity mặc định, và các breaking changes trong quản lý head. Mỗi phần bao gồm mã minh họa thực tế để các nhà phát triển có thể áp dụng trực tiếp vào dự án của mình. Đối với những ai đang chuẩn bị cho câu hỏi phỏng vấn Vue/Nuxt, việc nắm vững các khái niệm này sẽ là lợi thế cạnh tranh đáng kể trong năm 2026.

Di Chuyển Tự Động Với Codemod

Đội ngũ phát triển Nuxt đã hợp tác với Codemod để tự động hóa phần lớn các bước di chuyển. Chạy lệnh npx codemod@latest nuxt/4/migration-recipe để xử lý việc tái cấu trúc thư mục, cập nhật cú pháp data fetching, và thay thế các API không còn được hỗ trợ một cách tự động. Công cụ này phân tích mã nguồn hiện tại và áp dụng các phép biến đổi chính xác, giảm thiểu đáng kể thời gian và rủi ro trong quá trình nâng cấp.

Cấu Trúc Thư Mục app/ Mới Trong Nuxt 4

Thay đổi dễ nhận thấy nhất khi chuyển sang Nuxt 4 là việc toàn bộ mã nguồn ứng dụng được di chuyển vào thư mục app/. Sự phân tách này giải quyết một vấn đề thực tế mà nhiều dự án lớn gặp phải: các file watcher trên Linux và Windows hoạt động hiệu quả hơn đáng kể khi mã nguồn ứng dụng nằm trong một thư mục con riêng biệt, thay vì trộn lẫn với node_modules/, .git/, và các tệp cấu hình ở thư mục gốc.

Cấu trúc mới tuân theo sơ đồ sau:

text
my-nuxt-app/
├─ app/
│  ├─ assets/
│  ├─ components/
│  ├─ composables/
│  ├─ layouts/
│  ├─ middleware/
│  ├─ pages/
│  ├─ plugins/
│  ├─ utils/
│  ├─ app.vue
│  ├─ app.config.ts
│  └─ error.vue
├─ content/
├─ public/
├─ shared/
├─ server/
└─ nuxt.config.ts

Thư mục shared/ là một bổ sung đáng chú ý trong Nuxt 4. Bất kỳ composable, utility, hoặc type definition nào được đặt trong shared/ sẽ được auto-import trong cả ứng dụng Vue phía client lẫn Nitro server. Điều này loại bỏ nhu cầu import thủ công khi chia sẻ validation schema, hằng số, hoặc các hàm tiện ích giữa các ngữ cảnh khác nhau — một nhu cầu rất phổ biến trong các ứng dụng full-stack.

Việc phân tách này không chỉ cải thiện hiệu suất công cụ phát triển mà còn tạo ra ranh giới rõ ràng giữa logic nghiệp vụ và cấu hình dự án. Các nhà phát triển mới tham gia dự án có thể nhanh chóng định hướng được cấu trúc mã nguồn: app/ chứa toàn bộ giao diện người dùng, server/ chứa API endpoints, shared/ chứa code dùng chung, và các tệp cấu hình nằm ở thư mục gốc. Cấu trúc này cũng tương đồng với cách tổ chức của nhiều framework hiện đại khác, giúp giảm đường cong học tập cho các nhà phát triển chuyển đổi giữa các công nghệ.

Các Bước Di Chuyển Từ Nuxt 3 Sang Nuxt 4

Quá trình nâng cấp bắt đầu với lệnh upgrade chính thức. Nuxt 4 tự động phát hiện cấu trúc phẳng của Nuxt 3 và tiếp tục hoạt động bình thường, cho phép quá trình di chuyển diễn ra dần dần mà không gây gián đoạn.

bash
npx nuxt upgrade --dedupe

Lệnh này nâng cấp Nuxt lên phiên bản mới nhất và loại bỏ các dependency trùng lặp trong node_modules/. Flag --dedupe đặc biệt quan trọng vì Nuxt 4 yêu cầu các phiên bản nhất quán của Vue, Vue Router, và các package liên quan.

Sau khi nâng cấp package, bước tiếp theo là di chuyển các tệp ứng dụng vào thư mục app/:

bash
npx codemod@latest nuxt/4/file-structure

Codemod này tự động di chuyển assets/, components/, composables/, layouts/, middleware/, pages/, plugins/, utils/, app.vue, error.vue, và app.config.ts vào app/. Các tệp thuộc về thư mục gốc — bao gồm nuxt.config.ts, server/, public/, và content/ — vẫn giữ nguyên vị trí. Codemod cũng cập nhật các đường dẫn import tương đối nếu cần thiết.

Đối với các dự án lớn cần trì hoãn việc tái cấu trúc thư mục, có thể thiết lập cấu hình tường minh để Nuxt 4 hoạt động với cấu trúc phẳng:

nuxt.config.tstypescript
export default defineNuxtConfig({
  srcDir: '.',
  dir: { app: 'app' },
})

Cấu hình này yêu cầu Nuxt 4 phân giải các tệp từ thư mục gốc của dự án, khớp chính xác với hành vi của Nuxt 3. Đây là giải pháp tạm thời hữu ích cho các đội ngũ cần thời gian để kiểm tra và điều chỉnh CI/CD pipeline, đường dẫn deploy, hoặc cấu hình Docker trước khi áp dụng cấu trúc mới hoàn toàn.

Lớp Data Fetching Singleton Và Reactive Keys

Nuxt 4 thay đổi căn bản cách useAsyncDatauseFetch quản lý dữ liệu. Trong Nuxt 3, mỗi lần gọi composable tạo ra một reactive reference độc lập. Nuxt 4 chuyển sang mô hình singleton: nhiều component gọi cùng một key giờ đây chia sẻ một reactive reference duy nhất thay vì duy trì các bản sao riêng biệt.

app/composables/useProductData.tstypescript
export function useProductData(productId: string) {
  return useAsyncData(
    `product-${productId}`,
    () => $fetch(`/api/products/${productId}`),
    {
      getCachedData: (key, nuxtApp, ctx) => {
        if (ctx.cause === 'refresh:manual') return undefined
        return nuxtApp.payload.data[key]
      },
    },
  )
}

Ba thay đổi nổi bật trong lớp data mới này:

  • Shared refs: gọi useProductData('abc') trong hai component trả về cùng các ref data, error, và status. Cập nhật ở một nơi sẽ phản ánh tức thời ở nơi còn lại, loại bỏ hoàn toàn tình trạng dữ liệu không đồng bộ giữa các phần của giao diện.
  • Automatic cleanup: khi component cuối cùng sử dụng một key bị unmount, Nuxt giải phóng dữ liệu liên quan khỏi bộ nhớ, ngăn ngừa memory leak trong các ứng dụng SPA phức tạp.
  • Reactive keys: bọc key trong computed hoặc ref sẽ kích hoạt refetch tự động khi giá trị thay đổi, không cần viết watcher thủ công.

Callback getCachedData nhận thêm context object với thuộc tính cause ('initial', 'refresh:hook', 'refresh:manual', hoặc 'watch'), cho phép kiểm soát chi tiết khi nào cần phục vụ dữ liệu cache và khi nào cần fetch dữ liệu mới từ server. Đây là câu hỏi phỏng vấn thường gặp về caching strategy trong các ứng dụng Nuxt hiện đại.

Giá Trị Mặc Định Thay Đổi Từ null Sang undefined

Các giá trị dataerror từ useAsyncDatauseFetch giờ đây mặc định là undefined thay vì null như trong Nuxt 3. Cần rà soát và cập nhật tất cả các kiểm tra === null thành === undefined hoặc sử dụng so sánh lỏng (== null) để bao quát cả hai trường hợp. Thay đổi này ảnh hưởng đến cả template conditionals và logic trong <script setup>.

Shallow Reactivity Mặc Định Để Tối Ưu Hiệu Suất

Nuxt 4 chuyển data từ useAsyncDatauseFetch sang shallowRef thay vì ref. Vue không còn theo dõi đệ quy mọi thuộc tính lồng nhau, mang lại cải thiện hiệu suất đo lường được cho các phản hồi API với đối tượng lồng sâu hoặc mảng lớn. Đối với dashboard hiển thị hàng trăm bản ghi hoặc API trả về cấu trúc JSON phức tạp, sự thay đổi này giảm đáng kể overhead của hệ thống reactivity.

app/pages/dashboard.vuetypescript
<script setup lang="ts">
const { data: metrics } = await useFetch('/api/dashboard/metrics')

// Direct property mutation won't trigger reactivity
// metrics.value.visits = 100  // Won't trigger re-render

// Replace the entire value to trigger updates
metrics.value = { ...metrics.value, visits: 100 }

// Or opt into deep reactivity for this specific call
const { data: settings } = await useFetch('/api/settings', {
  deep: true,
})
</script>

Đối với hầu hết các trang hiển thị dữ liệu chỉ đọc — bao gồm dashboard, danh sách sản phẩm, và trang bài viết — shallow reactivity hoạt động hoàn toàn bình thường mà không cần bất kỳ thay đổi mã nào. Tùy chọn deep: true vẫn khả dụng cho các form hoặc giao diện tương tác cần mutate trực tiếp các thuộc tính lồng nhau.

Trong các buổi phỏng vấn kỹ thuật, câu hỏi về sự khác biệt giữa refshallowRef cùng tác động đến hiệu suất ứng dụng đang trở nên phổ biến hơn. Hiểu rõ cơ chế này giúp các nhà phát triển đưa ra quyết định đúng đắn về chiến lược quản lý trạng thái trong từng tình huống cụ thể. Chi tiết hơn về cơ chế rendering và hydration có thể tham khảo trong hướng dẫn SSR và static generation.

Sẵn sàng chinh phục phỏng vấn Vue.js / Nuxt.js?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

Phân Tách Ngữ Cảnh TypeScript Và Cải Thiện Type Safety

Nuxt 4 tạo ra các cấu hình TypeScript riêng biệt cho từng ngữ cảnh trong dự án, thay vì sử dụng một tsconfig.json chung cho toàn bộ mã nguồn như Nuxt 3. Sự phân tách này có ý nghĩa thực tiễn quan trọng: IDE không còn đề xuất các API chỉ dành cho server trong mã client, và ngược lại.

Bốn ngữ cảnh được phân tách bao gồm:

  • .nuxt/tsconfig.app.json cho mã nguồn ứng dụng Vue
  • .nuxt/tsconfig.server.json cho mã nguồn Nitro server
  • .nuxt/tsconfig.shared.json cho các tiện ích chia sẻ
  • .nuxt/tsconfig.node.json cho cấu hình thời điểm build

Một tsconfig.json duy nhất tại thư mục gốc của dự án tham chiếu đến cả bốn config:

json
{
  "files": [],
  "references": [
    { "path": "./.nuxt/tsconfig.app.json" },
    { "path": "./.nuxt/tsconfig.server.json" },
    { "path": "./.nuxt/tsconfig.shared.json" },
    { "path": "./.nuxt/tsconfig.node.json" }
  ]
}

Kiểm tra type trong CI cũng thay đổi. Lệnh vue-tsc giờ đây yêu cầu flag -b (build mode) để xử lý project references chính xác:

bash
# Before (Nuxt 3)
nuxt prepare && vue-tsc --noEmit

# After (Nuxt 4)
nuxt prepare && vue-tsc -b --noEmit

Một thay đổi TypeScript quan trọng khác: compilerOptions.noUncheckedIndexedAccess được đặt thành true theo mặc định. Truy cập phần tử mảng hoặc thuộc tính đối tượng theo index giờ đây trả về T | undefined, buộc các nhà phát triển phải xử lý trường hợp giá trị không tồn tại. Thay đổi này phát hiện nhiều lỗi runtime tiềm ẩn tại thời điểm compile, đặc biệt hữu ích cho các dự án enterprise có codebase lớn.

Đối với các dự án lớn, sự phân tách ngữ cảnh này cải thiện đáng kể thời gian phản hồi của IDE khi cung cấp autocompletion và phát hiện lỗi. Các nhà phát triển làm việc trên server code không còn bị phân tâm bởi các gợi ý từ client code, và ngược lại.

Chuẩn Hóa Tên Component Và Vue Router v5

Nuxt 4.3 đã nâng cấp lên Vue Router v5, loại bỏ sự phụ thuộc vào unplugin-vue-router. Đối với hầu hết các ứng dụng, việc nâng cấp này diễn ra minh bạch mà không yêu cầu thay đổi mã nguồn.

Quy ước đặt tên component giờ đây được chuẩn hóa nhất quán trong tất cả các ngữ cảnh. Một component tại components/dashboard/MetricsCard.vue sẽ có tên DashboardMetricsCard trong Vue DevTools, <KeepAlive>, và các tiện ích kiểm thử. Hành vi trước đây của Nuxt 3, nơi tên component có thể thay đổi tùy thuộc vào ngữ cảnh sử dụng, không còn áp dụng.

vue
<!-- app/pages/dashboard.vue -->
<template>
  <NuxtPage :keepalive="{
    include: ['DashboardMetricsCard', 'DashboardRecentActivity'],
  }" />
</template>

Các dự án sử dụng <KeepAlive> với bộ lọc tên component cần rà soát và cập nhật các tên để khớp với quy ước mới. Đặc biệt lưu ý rằng tên được tạo từ đường dẫn thư mục kết hợp với tên file, viết theo kiểu PascalCase. Ví dụ, components/admin/user/ProfileCard.vue sẽ có tên AdminUserProfileCard.

Trong các buổi phỏng vấn kỹ thuật về Vue và Nuxt, câu hỏi về cách <KeepAlive> hoạt động và cách tối ưu hóa hiệu suất component rendering thường xuyên xuất hiện. Hiểu rõ quy ước đặt tên mới giúp tránh các lỗi khó debug liên quan đến caching component, đặc biệt khi kết hợp với dynamic imports và lazy loading.

Xử Lý Breaking Changes Trong Quản Lý Head

Nuxt 4 đi kèm với Unhead v2, một bản nâng cấp lớn loại bỏ một số thuộc tính và hành vi không còn được hỗ trợ từ useHeaduseSeoMeta. Các thuộc tính vmid, hid, children, và body đã bị loại bỏ hoàn toàn. API useSeoMeta tiếp tục hỗ trợ reactive getters cho các giá trị động:

app/pages/product/[id].vuetypescript
<script setup lang="ts">
const route = useRoute()
const { data: product } = await useFetch(`/api/products/${route.params.id}`)

useSeoMeta({
  title: () => product.value?.name ?? 'Product',
  ogTitle: () => product.value?.name ?? 'Product',
  description: () => product.value?.description ?? '',
  ogImage: () => product.value?.imageUrl ?? '',
})
</script>

Đối với các dự án phụ thuộc vào template parameters (ví dụ %site.title%) hoặc alias sorting cho thẻ meta, cần cài đặt các plugin này một cách tường minh thay vì dựa vào hành vi mặc định của Nuxt 3:

app/plugins/unhead.tstypescript
import { TemplateParamsPlugin, AliasSortingPlugin } from '@unhead/vue/plugins'

export default defineNuxtPlugin({
  setup() {
    const unhead = injectHead()
    unhead.use(TemplateParamsPlugin)
    unhead.use(AliasSortingPlugin)
  },
})

Những thay đổi trong Unhead v2 phản ánh xu hướng chung của hệ sinh thái Vue hướng tới các API đơn giản hơn và dễ bảo trì hơn. Việc loại bỏ các thuộc tính lỗi thời giúp giảm kích thước bundle và cải thiện tính dự đoán của hành vi head management. Các nhà phát triển cần kiểm tra kỹ các trang quan trọng cho SEO sau khi nâng cấp để đảm bảo meta tags được render chính xác.

Lộ Trình Hỗ Trợ Nuxt 3

Nuxt 3 tiếp tục nhận các bản vá bảo mật và sửa lỗi nghiêm trọng cho đến ngày 31 tháng 7 năm 2026. Sau ngày đó, Nuxt 3 sẽ chính thức kết thúc vòng đời hỗ trợ và không còn nhận bất kỳ bản cập nhật nào. Các dự án đang chạy trên production nên lập kế hoạch di chuyển ngay từ bây giờ để tránh rủi ro vận hành ứng dụng trên framework đã hết hạn bảo trì.

Danh Sách Kiểm Tra Di Chuyển Và Các Lỗi Thường Gặp

Codemod tự động xử lý phần lớn các thay đổi, nhưng một số mục yêu cầu rà soát và can thiệp thủ công. Dưới đây là các điểm cần lưu ý:

  • Loại bỏ window.__NUXT__: thay thế bằng useNuxtApp().payload. Đối tượng global này bị xóa sau khi hydration hoàn tất trong Nuxt 4.
  • Hook pages:extend: chuyển sang hook pages:resolved mới, chạy sau khi quá trình quét page meta hoàn thành, đảm bảo thông tin route đầy đủ hơn.
  • Boolean dedupe trong refresh: thay thế refresh({ dedupe: true }) bằng refresh({ dedupe: 'cancel' })false bằng 'defer'. Kiểu boolean không còn được chấp nhận.
  • Inline styles: chỉ các style của Vue component được inline theo mặc định; CSS global tải dưới dạng các tệp riêng biệt. Thêm features: { inlineStyles: true } vào nuxt.config.ts để khôi phục hành vi Nuxt 3.
  • clearNuxtState: giờ đây đặt lại về giá trị khởi tạo thay vì undefined. Sử dụng clearNuxtState('key', { reset: false }) cho hành vi cũ.
  • Module bên thứ ba: một số module có thể chưa cập nhật để tương thích đầy đủ với Nuxt 4, đặc biệt là những module phụ thuộc vào các API nội bộ đã thay đổi.

Trình tự di chuyển thực tiễn cho các ứng dụng production:

  1. Chạy npx nuxt upgrade --dedupe và xác minh ứng dụng build thành công trên Nuxt 4
  2. Chạy codemod toàn diện: npx codemod@latest nuxt/4/migration-recipe
  3. Di chuyển các tệp vào app/ bằng file-structure codemod nếu chưa được thực hiện tự động
  4. Cập nhật các kiểm tra null thành undefined trong logic data fetching
  5. Kiểm tra các component <KeepAlive> với tên đã chuẩn hóa theo quy ước mới
  6. Cập nhật kiểm tra type trong CI pipeline sử dụng vue-tsc -b --noEmit
  7. Chạy toàn bộ test suite và sửa các lỗi TypeScript được phát hiện bởi noUncheckedIndexedAccess

Đối với các câu hỏi phỏng vấn liên quan đến quá trình migration và best practices, việc hiểu rõ từng bước trong trình tự trên cùng lý do đằng sau mỗi thay đổi sẽ thể hiện sự am hiểu sâu sắc về framework. Tham khảo thêm tại câu hỏi phỏng vấn Vue/Nuxt để ôn luyện các chủ đề thường gặp.

Kết Luận

Nuxt 4 mang đến một loạt cải tiến có ý nghĩa thực tiễn cho việc phát triển ứng dụng Vue full-stack, tập trung vào hiệu suất, type safety, và trải nghiệm phát triển tốt hơn:

  • Cấu trúc thư mục app/ phân tách rõ ràng mã nguồn ứng dụng khỏi cấu hình, cải thiện hiệu suất file watcher và giúp onboarding nhanh hơn cho các thành viên mới
  • Singleton data fetching loại bỏ request trùng lặp và đảm bảo tính nhất quán dữ liệu khi nhiều component sử dụng cùng một nguồn dữ liệu
  • Shallow reactivity mặc định thông qua shallowRef cải thiện hiệu suất cho các trang đọc nhiều mà không yêu cầu thay đổi mã trong hầu hết các trường hợp
  • Phân tách ngữ cảnh TypeScript (app, server, shared, node) loại bỏ rò rỉ type giữa các ngữ cảnh và tăng cường autocompletion trong IDE
  • Vue Router v5 và Unhead v2 mang đến các API sạch hơn, đi kèm với việc loại bỏ các thuộc tính không còn được hỗ trợ cần kiểm tra trong quá trình di chuyển
  • Nuxt 3 kết thúc vòng đời hỗ trợ vào ngày 31 tháng 7 năm 2026 — lập kế hoạch di chuyển sớm giúp đảm bảo ứng dụng luôn chạy trên phiên bản được bảo trì tích cực
  • Lệnh npx codemod@latest nuxt/4/migration-recipe tự động hóa phần lớn quá trình, giúp giảm thời gian và rủi ro cho các đội ngũ phát triển

Bắt đầu luyện tập!

Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.

Thẻ

#nuxt
#vue
#migration
#typescript
#tutorial

Chia sẻ

Bài viết liên quan