← Back to DevBytes

Migrating from Next.js to Nuxt: Step-by-Step Guide

Understanding the Migration: Next.js vs Nuxt

Both Next.js and Nuxt are powerful frameworks built on top of React and Vue respectively, offering server-side rendering, static site generation, file-based routing, and a rich ecosystem. While they share many concepts, the underlying philosophies, APIs, and project structures differ. Migrating from Next.js to Nuxt is a strategic decision often driven by a preference for Vue's reactivity system, the desire for a more opinionated and convention-driven architecture, or simply a team's skill set shift. This guide walks you through a complete, step-by-step migration process, covering everything from project setup to state management, data fetching, and deployment.

Preparing for Migration: Auditing Your Next.js App

πŸš€ Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Before writing a single line of Nuxt code, perform a thorough audit of your existing Next.js project. Identify all custom pages, API routes, middleware, layout patterns, and third-party integrations. Pay special attention to:

Document these pieces; you'll map each one to its Nuxt equivalent. This audit prevents surprises and ensures a feature-complete migration.

Step-by-Step Migration Process

1. Initialize a New Nuxt Project

Create a fresh Nuxt 3 project using the official starter. This gives you a clean, working foundation. You’ll copy over your application logic gradually.

# Using npx
npx nuxi@latest init my-nuxt-app

# Or with pnpm
pnpm dlx nuxi@latest init my-nuxt-app

cd my-nuxt-app

Install any additional dependencies you plan to reuse (e.g., Tailwind, Pinia, VueUse). The project comes with Nuxt 3's default directory structure:

my-nuxt-app/
β”œβ”€β”€ app.vue                 # main app entry (replaces _app.js)
β”œβ”€β”€ nuxt.config.ts          # unified configuration
β”œβ”€β”€ pages/                  # file-based routing
β”œβ”€β”€ components/             # auto-imported Vue components
β”œβ”€β”€ public/                 # static assets
β”œβ”€β”€ server/                 # server routes & middleware
└── .env                    # environment variables

2. Configure nuxt.config.ts – Mapping next.config.js

Next.js configuration is split across next.config.js and sometimes next.config.mjs. Nuxt centralizes everything in nuxt.config.ts. Here's a comparison of common settings:

// next.config.js (old)
module.exports = {
  reactStrictMode: true,
  images: {
    domains: ['example.com'],
  },
  env: {
    CUSTOM_KEY: 'my-value',
  },
  redirects: async () => [
    { source: '/old', destination: '/new', permanent: true },
  ],
};

// nuxt.config.ts (new)
export default defineNuxtConfig({
  // Strict mode equivalent is devtools feature, but no direct flag
  // Enable Vue reactivity transform if needed (optional)
  // images: use @nuxt/image module
  modules: ['@nuxt/image'],
  runtimeConfig: {
    public: {
      customKey: 'my-value', // accessible on client and server
    },
  },
  routeRules: {
    '/old': { redirect: '/new' },
  },
});

Move redirects and rewrites into routeRules. Environment variables become runtimeConfig. For image optimization, install @nuxt/image module. Tailwind, if used, goes into modules array via @nuxtjs/tailwindcss.

3. Migrate the Pages Directory – Routing

Both frameworks use a pages/ directory with file-based routing. The conventions are nearly identical, but Nuxt uses Vue components (.vue files) instead of React components (.jsx). The biggest difference is the component syntax itself. Here’s how a typical Next.js page maps to a Nuxt page:

// Next.js: pages/about.js
export default function About() {
  return 

About Us

Welcome!

; } // Nuxt: pages/about.vue <template> <div> <h1>About Us</h1> <p>Welcome!</p> </div> </template> <script setup> // Component logic here (optional) </script>

Dynamic routes translate directly:

// Next.js: pages/posts/[id].js
// Nuxt: pages/posts/[id].vue

// Catch-all: Next.js: pages/[...slug].js
// Nuxt: pages/[...slug].vue

Nested layouts in Next.js (using layout.tsx) become Nuxt layouts. Move your layout components to layouts/ directory and reference them with <NuxtLayout> or by setting layout: 'name' in page metadata. For example:

// layouts/default.vue
<template>
  <div>
    <TheHeader />
    <slot />
    <TheFooter />
  </div>
</template>

Then in a page:

<script setup>
definePageMeta({
  layout: 'default',
});
</script>

4. Migrate Data Fetching – From getStaticProps to Composables

Next.js offers getStaticProps, getServerSideProps, and getStaticPaths. Nuxt replaces these with powerful composables like useAsyncData, useFetch, and useLazyAsyncData. These work seamlessly on both server and client, and integrate with Nuxt’s hydration.

Example: fetching a list of posts at build time.

// Next.js: pages/blog.js
export async function getStaticProps() {
  const res = await fetch('https://api.example.com/posts');
  const posts = await res.json();
  return { props: { posts } };
}
export default function Blog({ posts }) { /* render */ }

// Nuxt: pages/blog.vue
<script setup>
const { data: posts } = await useAsyncData('posts', () =>
  $fetch('https://api.example.com/posts')
);
</script>
<template>
  <div v-for="post in posts" :key="post.id">{{ post.title }}</div>
</template>

The $fetch helper is Nuxt’s built-in HTTP client that works server-side without CORS issues. For client-side only fetching (like in a component), use useLazyAsyncData or plain useFetch with lazy: true.

For dynamic routes requiring data, combine useAsyncData with useRoute():

// Nuxt: pages/posts/[id].vue
<script setup>
const route = useRoute();
const { data: post } = await useAsyncData(`post-${route.params.id}`, () =>
  $fetch(`https://api.example.com/posts/${route.params.id}`)
);
</script>

5. Convert API Routes to Nuxt Server Routes

Next.js API routes live under pages/api. In Nuxt, server logic moves to server/api/ or server/routes/. The file name becomes the endpoint. The handler receives an event object with getQuery, readBody, etc., instead of Express-like req/res.

// Next.js: pages/api/hello.js
export default function handler(req, res) {
  res.status(200).json({ message: 'Hello' });
}

// Nuxt: server/api/hello.ts
export default defineEventHandler(() => {
  return { message: 'Hello' };
});

For POST with body:

// Next.js
export default async function handler(req, res) {
  const body = req.body;
  // process
  res.status(201).json({ id: 1 });
}

// Nuxt
export default defineEventHandler(async (event) => {
  const body = await readBody(event);
  // process
  setResponseStatus(event, 201);
  return { id: 1 };
});

Middleware in Next.js (middleware.ts) translates to server/middleware/ in Nuxt. The file runs on every request and can modify the response or check authentication.

// Next.js: middleware.ts
import { NextResponse } from 'next/server';
export function middleware(request) {
  if (!request.cookies.get('token')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
}

// Nuxt: server/middleware/auth.ts
export default defineEventHandler((event) => {
  const { token } = parseCookies(event);
  if (!token) {
    return sendRedirect(event, '/login');
  }
});

6. Styling and CSS Modules

Nuxt supports global CSS, CSS Modules, Tailwind, and any preprocessor out of the box. If you used CSS Modules in Next.js (styles.module.css), rename them to .module.css and import them in the <script> or <style> block. Vue’s scoped styles often replace CSS Modules entirely, giving component-scoped CSS without extra file extensions.

// Next.js component with CSS Modules
import styles from './Button.module.css';
export default function Button() {
  return ;
}

// Nuxt component with scoped styles
<template>
  <button class="primary">Click</button>
</template>
<style scoped>
.primary {
  background-color: blue;
}
</style>

To use Tailwind, install the module and add it to nuxt.config.ts:

npm install @nuxtjs/tailwindcss
// nuxt.config.ts
modules: ['@nuxtjs/tailwindcss']

Then your Tailwind classes work directly in templates. Global CSS files can be imported via the css property in config.

7. State Management – From React Context/Redux to Pinia

Nuxt has first-class support for Pinia, the Vue equivalent of Redux/Zustand. If you used React Context or Redux, migrate the store logic to Pinia stores. Pinia provides a more intuitive API with actions, getters, and state. Nuxt auto-imports stores defined in stores/ directory.

// Next.js (React Context) – UserProvider.js
const UserContext = createContext();
export function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  return {children};
}

// Nuxt: stores/user.ts
export const useUserStore = defineStore('user', () => {
  const user = ref(null);
  function setUser(newUser) {
    user.value = newUser;
  }
  return { user, setUser };
});

Then use it in any component without imports:

<script setup>
const userStore = useUserStore();
</script>

For larger state with Redux, consider Pinia modules. The pattern maps naturally.

8. Migrate Components – React to Vue Translation

Convert each React component to a Vue single-file component (.vue). The mental model is similar, but syntax changes:

Example conversion of a button component:

// Next.js: components/Button.jsx
export default function Button({ variant, onClick, children }) {
  return ;
}

// Nuxt: components/Button.vue
<template>
  <button :class="`btn-${variant}`" @click="$emit('click')">
    <slot />
  </button>
</template>
<script setup>
defineProps({
  variant: String,
});
defineEmits(['click']);
</script>

Nuxt auto-imports components from the components/ directory, so you don't need to manually import them.

9. Authentication and Protected Routes

Next.js middleware often handles route protection. In Nuxt, you can use server middleware (as shown) for API protection, and for client-side page guards, leverage useRouter in a layout or a global navigation guard. Nuxt provides a pages/ middleware concept using definePageMeta with middleware property, similar to Next.js middleware.ts but page-specific.

// Nuxt page middleware: middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
  const user = useUserStore();
  if (!user.isLoggedIn) {
    return navigateTo('/login');
  }
});

// In a page
<script setup>
definePageMeta({
  middleware: 'auth',
});
</script>

Place the middleware file in middleware/ directory (not server). This runs on client side during navigation and on server during initial render if SSR.

Best Practices for a Smooth Migration

Conclusion

Migrating from Next.js to Nuxt is a structured, manageable process when approached systematically. By mapping routing, data fetching, server logic, and component patterns one by one, you can fully transform a React-based application into a Vue-powered Nuxt app without losing functionality or performance. The key is to treat the migration as a refactor with a new framework, not a complete rewrite from scratch. With Nuxt’s powerful composables, auto-imports, and unified configuration, your final codebase will likely be more concise and convention-driven. Start with a thorough audit, follow the steps outlined above, and leverage best practices to ensure a seamless transition.

πŸš€ Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started β€” $23.99/mo
← Back to all articles