Most companies hiring a senior Vue engineer in 2026 have a codebase with history: Options API components from 2019, a Vuex store, maybe a Nuxt 2 app still running somewhere. "Have you migrated a large codebase" is a real question, and so is "Options or Composition, and why". The good answer is architectural, not stylistic.
data, computed, methods, watch); Composition API groups it by feature and lets you extract a feature into a composable. Both run on the same reactivity system; this.count and count.value are the same Proxy underneath.Vue.extend, better tree-shaking (unused APIs are not bundled), and <script setup> compiles to less code than a component options object.__VUE_OPTIONS_API__: false), which removes its runtime from the bundle Nuxt 5 . Existing Options components in a Nuxt 5 app need the flag re-enabled or a migration.<script setup> macros: defineProps, defineEmits, defineModel, defineExpose, defineSlots, defineOptions, withDefaults. Top-level await makes the component async, which requires a <Suspense> ancestor; Nuxt pages have one.createApp instead of new Vue; fragments (multiple roots) and therefore inheritAttrs matters more; v-model on components uses modelValue/update:modelValue and .sync is gone; the emits option; filters removed; $children, $on/$off (the event bus) and Vue.set removed because Proxies track everything; destroyed → unmounted; transition classes v-enter-from; v-if now has priority over v-for; key goes on <template v-for>; async components via defineAsyncComponent; Vuex → Pinia; Vue Router 4's createRouter and guard return values; IE11 dropped.app/ becomes the source directory, shared/ appears, useAsyncData/useFetch default data to undefined and are shallow-reactive, request deduplication changed (dedupe), route metadata and component names are normalised, compatibilityVersion: 4 on Nuxt 3.12+ let you opt in early. Nuxt 3 reached end of life on 31 July 2026. A codemod recipe (npx codemod@latest nuxt/4/migration-recipe) handles the mechanical parts.nuxt/server, web-standard Request/Response in Nitro 3 and h3 v2, the Vite Environment API, Options API off, typed pages on by default, case-sensitive routing, callHook no longer awaited. The landscape section has the full list and the readiness plan.The same component in both styles, which is the fastest way to explain the difference:
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
props: { userId: { type: String, required: true } },
emits: ['saved'],
data: () => ({ name: '', saving: false }),
computed: {
canSave(): boolean { return this.name.trim().length > 0 && !this.saving },
},
watch: {
userId: { handler: 'load', immediate: true },
},
methods: {
async load() { this.name = (await $fetch<{ name: string }>(`/api/users/${this.userId}`)).name },
async save() {
this.saving = true
await $fetch(`/api/users/${this.userId}`, { method: 'PATCH', body: { name: this.name } })
this.saving = false
this.$emit('saved')
},
},
})
</script>
<script setup lang="ts">
const { userId } = defineProps<{ userId: string }>()
const emit = defineEmits<{ saved: [] }>()
// One feature, one block. Extractable into useUserName(userId) verbatim.
const name = ref('')
const saving = ref(false)
const canSave = computed(() => name.value.trim().length > 0 && !saving.value)
watch(() => userId, async (id) => {
name.value = (await $fetch<{ name: string }>(`/api/users/${id}`)).name
}, { immediate: true })
async function save() {
saving.value = true
await $fetch(`/api/users/${userId}`, { method: 'PATCH', body: { name: name.value } })
saving.value = false
emit('saved')
}
</script>
A mixin becoming a composable, the migration people actually do most often:
// Before: a mixin adding `page`, `perPage`, `pageCount` and `next()` to any component,
// with no types and a silent collision if the component already had `page`.
export function usePagination(total: MaybeRefOrGetter<number>, perPage = 20) {
const page = ref(1)
const pageCount = computed(() => Math.max(1, Math.ceil(toValue(total) / perPage)))
const next = () => { page.value = Math.min(page.value + 1, pageCount.value) }
const prev = () => { page.value = Math.max(page.value - 1, 1) }
return { page, pageCount, next, prev }
}
Nobody funds a rewrite for aesthetics. The arguments that land: Nuxt 3 is end-of-life and no longer receives security releases; Options API support is compiled out in Nuxt 5; mixin-based code cannot be typed; the composable extraction makes the logic testable. Bring numbers: components counted, test coverage on the critical flows, an estimate per feature, and a first slice that ships in a week.
The Nuxt 5 release status and the exact list of breaking changes; both move between nightlies. Also confirm whether Options API is disabled by default in the released version.
docs ↗<script setup>; then extract its feature into a composable and write one Vitest test for it.npx codemod@latest nuxt/4/migration-recipe on a copy of a Nuxt 3 project, read the diff, and note which changes it could not make."Composition API, for architectural reasons rather than taste. It groups code by feature so a component reads as a list of concerns, and each concern can be lifted into a composable without the implicit sources and name collisions of mixins. It types fully with plain TypeScript inference, it tree-shakes, and <script setup> compiles to less code. Options API is fine for small components and is not deprecated in Vue 3, but Nuxt 5 compiles its runtime out by default, so I treat remaining Options components as migration backlog."
"Measure, protect, then move incrementally. Measure: count components, find the patterns that will not port mechanically, mixins, filters, $on buses, Vue.set. Protect: E2E tests on the critical flows, typecheck in CI, a hydration-warning test if it is SSR. Then migrate one route or feature at a time behind those tests, using the compat build and codemods for the mechanical parts and doing the mixin-to-composable work by hand. I keep the business case explicit, end-of-life dates, security releases, hiring, and I ship a first slice within the first week so the migration is real rather than a plan."
Nothing yet. Notes filed here show up on this page and the section overview.
Patterns and anti-patterns
Renderless and headless components, compound components, controlled versus uncontrolled state, composable-first components, and the smells a senior spots in a code review.
Vue 3.6 and Vapor mode
What Vue 3.5 added, what the 3.6 release candidate changes (Vapor mode, the alien-signals reactivity core), what stays the same, and how to talk about it without overclaiming.