Skip to content
ExponaBLOG
VUE.JS & NUXT · DAY 070 / 100

Nuxt interview questions: a hands-on guide for engineers

Nuxt interview questions about rendering modes, data fetching, hydration and server routes — with strong answers.

Expona Admin8 min read

You're sitting across from an interviewer who asks: "Walk me through how Nuxt renders your application—when would you use SSR over SSG?" These Nuxt interview questions reveal whether you understand the fundamentals that separate junior developers from those shipping production applications at scale. This guide covers rendering modes, data fetching strategies, hydration pitfalls, and server routes—the core topics that come up in interviews and that we encounter daily when building real applications.

SSR vs SSG vs Hybrid: Picking the Right Rendering Mode

When an interviewer asks about rendering modes, they're testing whether you understand the trade-offs between server-side rendering, static site generation, and Nuxt 3's hybrid rendering. Each mode serves different use cases, and the wrong choice will cost you in performance, build time, or user experience.

Server-side rendering (SSR) means every request triggers a full render on the server. The server runs your Vue components, generates HTML, and sends it to the browser. The browser receives a fully rendered page and interactive UI simultaneously. This works well for applications where content changes frequently, user sessions matter, or you need real-time personalization. Think of an e-commerce product page where inventory or pricing updates constantly—SSR ensures each visitor sees current data.

Static site generation (SSG), also called pre-rendering, builds your entire site at deploy time. Every route becomes a static HTML file. When a user visits, they download pre-built HTML with no server compute required. This is blazing fast but only works when your content doesn't change between deploys. A blog, documentation site, or marketing site with infrequent updates fits perfectly here.

Hybrid rendering is Nuxt 3's superpower. You define route rules that dictate how each route behaves: some routes pre-render as static files, others use SSR on-demand, and some cache responses. This lets you combine the speed of static files with the flexibility of server rendering.

Here's a realistic config for a SaaS platform:

typescript
// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    // Pre-render marketing pages at build time
    '/': { prerender: true },
    '/pricing': { prerender: true },
    '/docs/**': { prerender: true },
    
    // Cache authenticated user dashboards for 1 hour
    '/dashboard/**': { cache: { maxAge: 60 * 60 } },
    
    // Dynamic product pages—SSR on-demand, cache for 24 hours
    '/products/**': { cache: { maxAge: 60 * 60 * 24 } },
    
    // Admin panel: always fresh, no caching
    '/admin/**': { cache: false },
    
    // Redirect old URLs
    '/old-page': { redirect: '/new-page' }
  }
});

In production, we've used this exact pattern to serve thousands of product pages without rebuilding the entire site. Marketing pages load instantly because they're static. Product listings refresh daily via cache invalidation. Admin dashboards always show fresh data. One config, three different strategies.

useFetch vs $fetch: When to Use Each

This question separates people who've debugged hydration issues from those who haven't. Both fetch data, but they work in fundamentally different contexts.

useFetch is a composable that runs on both server and client. Call it in your component, and Nuxt automatically fetches data during SSR, serializes it, and sends it to the client. The client re-hydrates with that same data—no duplicate request. This is what you want in most scenarios.

typescript
// pages/products/[id].vue
export default defineComponent({
  async setup(props) {
    // Runs on server during SSR, runs on client during navigation
    const { data: product } = await useFetch(
      `/api/products/${props.route.params.id}`
    );
    
    return { product };
  }
});

$fetch is a simple HTTP client that only runs where you explicitly call it. It doesn't integrate with Nuxt's SSR pipeline. Use it for client-side only operations—button clicks, form submissions, after-render data updates.

typescript
// pages/checkout.vue
export default defineComponent({
  methods: {
    async submitOrder() {
      // Runs only on client, after hydration
      const result = await $fetch('/api/orders', {
        method: 'POST',
        body: { items: this.cart }
      });
      this.orderId = result.id;
    }
  }
});

Here's where many developers go wrong: they use $fetch in component setup, expecting data during SSR. It won't work. The server doesn't execute client-only code, so your data never loads on the server side, and you get a blank page until hydration completes.

A practical example: we built an analytics dashboard for a SaaS client. Initial metrics came via useFetch (pre-rendered on the server). User-triggered filters used $fetch (client-side only). This gave us the best of both—fast initial load, responsive interactions.

How to Avoid Hydration Mismatches

Hydration mismatches happen when the HTML rendered on the server doesn't match what the client renders. The browser gets confused, Vue throws errors, and interactivity breaks. This is one of the most common production issues we see, and it's usually preventable.

The core rule: server and client must render identical HTML. Any difference causes a mismatch.

Common culprits:

  • Timestamps and dates: If the server renders "Posted 2 hours ago" but the client renders "Posted 2 hours and 5 seconds ago" (because time passed), you have a mismatch. Use useAsyncData with a key and rely on the server's timestamp, or format dates consistently.
  • Random values: Math.random() or UUID generation that differs between server and client breaks hydration. Pre-generate random values on the server and pass them to the client.
  • Browser-only APIs: Calling window, document, or localStorage during setup runs on the client but crashes on the server. Wrap these in if (process.client) guards or use <ClientOnly> components.
  • Conditional rendering based on device: If the server assumes desktop but the client detects mobile, you get different HTML. Use CSS media queries or detect after hydration.

Here's a real scenario we fixed: a product listing showed "In stock" or "Out of stock" based on real-time inventory. The server rendered one state, but by the time the client hydrated, inventory changed. We switched to fetching the state via useFetch with a cache strategy, ensuring server and client both see the same version.

typescript
// pages/product.vue
export default defineComponent({
  setup() {
    // Good: Server and client both fetch the same data
    const { data: product } = useFetch('/api/product/123');
    
    // Bad: Different between server and client
    const timestamp = ref(new Date().toISOString()); // ❌
    
    // Good: Server generates it, client receives it
    const randomId = ref('');
    onMounted(() => {
      randomId.value = Math.random().toString(36).slice(2);
    });
    
    return { product, randomId };
  }
});

Use the <ClientOnly> wrapper for components that must run only on the client—charts that need window dimensions, animations, or interactive widgets that don't need SSR.

How Server Routes Work

Nuxt server routes (API routes) let you build backend logic without a separate server. Put files in server/api/ and Nuxt exposes them as HTTP endpoints. This is powerful for small projects or adding API layers to static frontends.

A server route is a simple function that receives a request and returns a response:

typescript
// server/api/products/[id].ts
export default defineEventHandler(async (event) => {
  const { id } = getRouterParams(event);
  
  // Fetch from your database
  const product = await db.products.findById(id);
  
  if (!product) {
    throw createError({
      statusCode: 404,
      statusMessage: 'Product not found'
    });
  }
  
  return product;
});

The URL becomes /api/products/123. Your frontend calls it:

typescript
// In a component
const { data: product } = useFetch('/api/products/123');

Server routes run only on the server, never on the client. This is critical for security—you can access environment variables, database credentials, and authentication tokens without exposing them to the browser.

Here's a practical example from a trading platform we built. Client-side charts showed real-time data via WebSocket, but historical data came through a server route that queried the database. The server route could access the trading API key (stored as an env var) without the key ever reaching the browser:

typescript
// server/api/trades/history.ts
export default defineEventHandler(async (event) => {
  // This API key never leaves the server
  const apiKey = process.env.TRADING_API_KEY;
  
  const userId = await requireAuth(event); // Auth middleware
  const trades = await tradingService.getUserTrades(userId, apiKey);
  
  return trades;
});

Server routes also handle middleware, authentication, and logging naturally. You get full control over what hits your backend.

Common Mistakes

  • Mixing $fetch with SSR data: Using $fetch to load initial page data breaks SSR. Use useFetch for anything you need server-side.
  • Ignoring cache headers: Even with SSR, set proper Cache-Control headers. Let CDNs cache static responses. We've seen 10x speed improvements from getting this right.
  • Rendering different content server vs client: Dates, timestamps, and user-agent detection must match. Always think: "Will the server and client render the same HTML?"
  • Hardcoding routes in server files: Server routes are HTTP endpoints. They need proper error handling, validation, and logging like any API.
  • Over-caching dynamic routes: Pre-rendering a dashboard or user-specific page breaks multi-user scenarios. Know which routes change per-user and set cache policies accordingly.

Key Takeaways

  • Choose your rendering mode intentionally: SSR for dynamic content, SSG for static content, hybrid for a mix. Use Nuxt's route rules to combine strategies in one config.
  • useFetch is your default for page data: It integrates with SSR and prevents duplicate requests. Use $fetch only for client-side interactions like form submissions.
  • Hydration mismatches come from server/client inconsistencies: Guard against timestamps, random values, and browser APIs. Test with npm run build && npm run preview to catch these before production.
  • Server routes are secure API endpoints: They run only on the server, so environment variables and secrets stay private. Use them for database access, third-party API calls, and authentication.
  • Cache strategically with route rules: Pre-render what's static, cache what changes slowly, leave dynamic routes fresh. One config can handle all three patterns.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles