kb.erickguedes.com
Nitro: Servidor Universal para UnJS

Renderização Híbrida e Route Rules

Aula 2 de 4

Hybrid Rendering

Nitro permite diferentes modos de renderização para diferentes rotas no mesmo projeto.

Modos de Renderização

ModoDescrição
serverRenderizado sob demanda (SSR tradicional)
clientSPA renderizado no cliente
staticPré-renderizado no build (HTML estático)
prerenderIgual static, mas força pré-renderização
// nitro.config.ts
import { defineNitroConfig } from 'nitro/config';

export default defineNitroConfig({
  routeRules: {
    // Home: estática (pré-renderizada)
    '/': { prerender: true },

    // Blog: ISR (revalida a cada hora)
    '/blog/**': { isr: 3600 },

    // Dashboard: sempre server-side
    '/dashboard/**': { ssr: true },

    // Admin SPA: client-side rendering
    '/admin/**': { ssr: false },

    // API: sempre server-side
    '/api/**': { ssr: true },

    // Redirect
    '/old-page': { redirect: '/new-page' },

    // CORS para API
    '/api/**': { cors: true }
  }
});

Prerender

// nitro.config.ts
export default defineNitroConfig({
  prerender: {
    crawlLinks: true,        // Segue links para pré-renderizar
    routes: ['/', '/about', '/contact'],
    ignore: ['/api/**', '/admin/**']
  }
});
// server/routes/about.ts — será pré-renderizado
export default defineEventHandler(() => {
  return {
    title: 'Sobre Nós',
    content: 'Página estática gerada no build'
  };
});

Route Rules

Route rules permitem configurar comportamento específico por padrão de URL.

// nitro.config.ts
export default defineNitroConfig({
  routeRules: {
    // ISR - Incremental Static Regeneration
    '/products/**': {
      isr: 300 // revalida a cada 5 minutos
    },

    // SWR - Stale While Revalidate
    '/blog/**': {
      swr: 3600,            // serve cache por 1 hora
      isr: 3600             // revalida em background
    },

    // Static
    '/': { prerender: true },

    // Redirect (301)
    '/old-blog/**': {
      redirect: {
        to: '/blog/**',
        statusCode: 301
      }
    },

    // Proxy
    '/legacy-api/**': {
      proxy: {
        to: 'https://old-server.com/api/**',
        opts: {
          headers: { 'X-Proxy': 'nitro' }
        }
      }
    },

    // Headers customizados
    '/secure/**': {
      headers: {
        'X-Frame-Options': 'DENY',
        'Content-Security-Policy': "default-src 'self'"
      }
    },

    // CORS
    '/api/**': {
      cors: true,
      headers: {
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE'
      }
    }
  }
});

Route Rules com Cache

// nitro.config.ts
export default defineNitroConfig({
  routeRules: {
    // Cache no servidor
    '/api/public/**': {
      cache: {
        maxAge: 300,         // 5 minutos no CDN
        swr: true,           // serve stale enquanto revalida
        headersOnly: true    // cache apenas se headers permitirem
      }
    },

    // Sem cache
    '/api/private/**': {
      cache: false
    }
  }
});

Cache

Nitro possui sistema de cache interno e integração com CDNs.

// server/api/products.ts
export default defineEventHandler(async (event) => {
  // Cache manual com event.caches (se disponível)
  const cache = event.caches?.('products');
  const cached = await cache?.match(event.path);
  if (cached) return cached;

  const products = await fetchProducts();

  // Cache para 5 minutos, serve stale enquanto revalida
  setResponseHeader(event, 'Cache-Control', 'public, max-age=300, stale-while-revalidate=3600');

  return products;
});

Cache com Storage

// server/middleware/cache.ts
export default defineEventHandler(async (event) => {
  const url = event.path;
  const cacheKey = `cache:${url}`;

  // Tentar ler do cache (Redis, filesystem, etc)
  const cached = await useStorage('cache').getItem(cacheKey);
  if (cached) {
    setResponseHeader(event, 'X-Cache', 'HIT');
    return cached;
  }

  // Se cache miss, processar normalmente
  setResponseHeader(event, 'X-Cache', 'MISS');
});

Render Modes Combinados

// nitro.config.ts
export default defineNitroConfig({
  routeRules: {
    // Blog posts: ISR com cache
    '/blog/**': {
      isr: 60,
      swr: true,
      prerender: true // pré-renderiza os principais
    },

    // Admin: SPA client-side
    '/admin/**': {
      ssr: false
    },

    // Landing pages: estáticas
    '/': { prerender: true },
    '/features': { prerender: true },
    '/pricing': { prerender: true },

    // API: server + CORS
    '/api/**': {
      ssr: true,
      cors: true
    }
  },

  // Dev options
  devServer: {
    watch: ['server/**']
  }
});

useNitroApp

// server/plugins/hooks.ts
export default defineNitroPlugin((nitroApp) => {
  // Hook executado antes de cada requisição
  nitroApp.hooks.hook('request', (event) => {
    console.log(`[${event.method}] ${event.path}`);
  });

  // Hook após resposta
  nitroApp.hooks.hook('afterResponse', (event, response) => {
    console.log(`Response: ${response.status}`);
  });
});
// server/api/use-nitro.ts
export default defineEventHandler(async (event) => {
  // Acessar o app Nitro
  const app = useNitroApp();

  // Executar outro handler internamente
  const result = await app.callHandler(event, '/api/products');

  // Acessar configuração
  const config = useRuntimeConfig();
  return {
    appVersion: config.appVersion,
    result
  };
});

Lab: Exercício - Site Híbrido

// nitro.config.ts
export default defineNitroConfig({
  routeRules: {
    '/': { prerender: true },
    '/about': { prerender: true },
    '/blog': { prerender: true },
    '/blog/**': { isr: 300 },
    '/api/**': { ssr: true, cors: true },
    '/dashboard/**': { ssr: true }
  },
  prerender: {
    crawlLinks: true,
    routes: ['/', '/about', '/blog']
  }
});
// server/api/posts.ts
export default defineEventHandler(async () => {
  // Simula busca de dados
  const posts = [
    { slug: 'intro-nitro', title: 'Introdução ao Nitro' },
    { slug: 'route-rules', title: 'Route Rules Explicadas' }
  ];

  setResponseHeader(event, 'Cache-Control', 'public, max-age=300');
  return posts;
});
# Build (pré-renderiza as rotas configuradas)
npm run build

# Em produção, / e /about são HTML estático
# /blog é estático, mas posts individuais são ISR
# /api é SSR normal com CORS

Route rules no Nitro permitem configurar diferentes estratégias de renderização por URL. Combine prerender (rotas estáticas), isr (cache com revalidação) e ssr (dinâmico) no mesmo projeto. Cache headers e CORS são configurados centralmente.