Inertia.js sem Laravel: usando o protocolo com outros backends
1. Entendendo o Protocolo Inertia.js além do Laravel
Inertia.js é, em sua essência, um protocolo de comunicação entre frontend e backend, não um framework completo. O Laravel é apenas a implementação mais madura desse protocolo. O cerne do Inertia reside em headers HTTP específicos e um formato JSON padronizado.
Os headers fundamentais são:
- X-Inertia: true — indica que a requisição espera uma resposta Inertia
- X-Inertia-Version: [hash] — versão dos assets frontend para controle de cache
- X-Inertia-Partial-Data: component,prop1,prop2 — usado em partial reloads
A resposta JSON deve seguir esta estrutura:
{
"component": "Users/List",
"props": {
"users": [...],
"flash": { "success": "Usuário criado" }
},
"url": "/users",
"version": "abc123"
}
2. Configuração do Backend Node.js (Express/Fastify)
Vamos implementar um middleware Inertia mínimo para Express:
// middleware/inertia.js
const inertiaVersion = require('fs').readFileSync('public/mix-manifest.json', 'utf8');
function inertia(component, props = {}) {
return (req, res) => {
const version = JSON.parse(inertiaVersion)['/js/app.js'].hash;
// Validação de versão (cache busting)
if (req.headers['x-inertia-version'] !== version) {
return res.status(409).json({
component: component,
props: {},
url: req.originalUrl,
version: version
});
}
// Resposta Inertia padrão
res.setHeader('X-Inertia', 'true');
res.json({
component,
props: {
...props,
flash: req.session?.flash || {}
},
url: req.originalUrl,
version
});
};
}
function redirect(url, status = 302) {
return (req, res) => {
if (req.headers['x-inertia']) {
res.status(409).setHeader('X-Inertia-Location', url).end();
} else {
res.redirect(status, url);
}
};
}
Uso em uma rota:
const express = require('express');
const app = express();
app.get('/users', inertia('Users/List', {
users: [{ id: 1, name: 'Alice' }]
}));
app.post('/users', (req, res) => {
// lógica de criação
req.session.flash = { success: 'Usuário criado' };
redirect('/users')(req, res);
});
3. Backend em Python (Flask ou FastAPI)
Implementação com Flask:
# inertia_adapter.py
from flask import jsonify, request, session, redirect as flask_redirect
import hashlib
import json
class Inertia:
def __init__(self, app, version_callback=None):
self.app = app
self.version_callback = version_callback or (lambda: '1.0.0')
self.shared_props = {}
def render(self, component, props=None):
props = props or {}
version = self.version_callback()
# Partial reload handling
if request.headers.get('X-Inertia-Partial-Data'):
only = request.headers['X-Inertia-Partial-Data'].split(',')
if request.headers.get('X-Inertia-Partial-Component') == component:
props = {k: v for k, v in props.items() if k in only}
# Version check
if request.headers.get('X-Inertia-Version') != version:
return self._version_conflict(component, props, version)
response = jsonify({
'component': component,
'props': {**self.shared_props, **props, 'flash': session.get('_flash', {})},
'url': request.url,
'version': version
})
response.headers['X-Inertia'] = 'true'
return response
def _version_conflict(self, component, props, version):
response = jsonify({
'component': component,
'props': {},
'url': request.url,
'version': version
})
response.status_code = 409
return response
def redirect(self, url, status=302):
if request.headers.get('X-Inertia'):
response = self.app.response_class(status=409)
response.headers['X-Inertia-Location'] = url
return response
return flask_redirect(url, status)
def share(self, key, value):
self.shared_props[key] = value
Exemplo de CRUD:
from flask import Flask, session
from inertia_adapter import Inertia
app = Flask(__name__)
inertia = Inertia(app, version_callback=lambda: '2.0.0')
@app.route('/users')
def list_users():
users = [{'id': 1, 'name': 'Alice'}, {'id': 2, 'name': 'Bob'}]
return inertia.render('Users/List', {'users': users})
@app.route('/users/create', methods=['POST'])
def create_user():
session['_flash'] = {'success': 'Usuário criado com sucesso!'}
return inertia.redirect('/users')
4. Backend em Go (Gin ou Chi)
Adapter mínimo para Gin:
package inertia
import (
"crypto/sha256"
"encoding/json"
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
type Inertia struct {
version string
shared map[string]interface{}
}
func New(version string) *Inertia {
return &Inertia{
version: version,
shared: make(map[string]interface{}),
}
}
type Response struct {
Component string `json:"component"`
Props map[string]interface{} `json:"props"`
URL string `json:"url"`
Version string `json:"version"`
}
func (i *Inertia) Render(c *gin.Context, component string, props map[string]interface{}) {
if c.GetHeader("X-Inertia-Version") != i.version {
c.JSON(http.StatusConflict, Response{
Component: component,
Props: map[string]interface{}{},
URL: c.Request.URL.String(),
Version: i.version,
})
return
}
// Merge shared props
finalProps := make(map[string]interface{})
for k, v := range i.shared {
finalProps[k] = v
}
for k, v := range props {
finalProps[k] = v
}
c.Header("X-Inertia", "true")
c.JSON(http.StatusOK, Response{
Component: component,
Props: finalProps,
URL: c.Request.URL.String(),
Version: i.version,
})
}
func (i *Inertia) Redirect(c *gin.Context, url string) {
if c.GetHeader("X-Inertia") == "true" {
c.Header("X-Inertia-Location", url)
c.Status(http.StatusConflict)
return
}
c.Redirect(http.StatusFound, url)
}
func (i *Inertia) Share(key string, value interface{}) {
i.shared[key] = value
}
5. Frontend: Adaptação do Cliente Inertia.js
Configuração com React sem Laravel:
// app.js
import { createInertiaApp } from '@inertiajs/react'
import { createRoot } from 'react-dom/client'
import { InertiaProgress } from '@inertiajs/progress'
createInertiaApp({
resolve: name => {
const pages = import.meta.glob('./Pages/**/*.jsx', { eager: true })
return pages[`./Pages/${name}.jsx`]
},
setup({ el, App, props }) {
createRoot(el).render(<App {...props} />)
},
progress: {
delay: 250,
color: '#29d',
},
})
Tratamento de erros no cliente:
// errors.js
import { router } from '@inertiajs/react'
router.on('error', (event) => {
const { response } = event.detail
if (response.status === 409) {
// Forçar recarregamento da página por conflito de versão
window.location.reload()
}
})
6. Autenticação e Sessão sem Laravel
Implementação de login com JWT:
// backend (Express)
const jwt = require('jsonwebtoken')
app.post('/login', (req, res) => {
const { email, password } = req.body
// validar credenciais
const token = jwt.sign({ userId: user.id }, 'secret', { expiresIn: '7d' })
res.cookie('token', token, { httpOnly: true, sameSite: 'strict' })
inertia.redirect('/dashboard')(req, res)
})
// Compartilhar dados de sessão
app.use((req, res, next) => {
if (req.cookies.token) {
try {
const decoded = jwt.verify(req.cookies.token, 'secret')
inertia.share('auth', { user: { name: decoded.name } })
} catch (e) {
inertia.share('auth', null)
}
}
next()
})
7. Testes e Debug do Protocolo
Testes unitários para o middleware:
// test/inertia.test.js
const request = require('supertest')
const app = require('../app')
describe('Inertia Middleware', () => {
test('deve retornar 409 se versão divergir', async () => {
const res = await request(app)
.get('/users')
.set('X-Inertia', 'true')
.set('X-Inertia-Version', 'old-version')
expect(res.status).toBe(409)
expect(res.body.version).toBeDefined()
})
test('deve retornar JSON com estrutura correta', async () => {
const res = await request(app)
.get('/users')
.set('X-Inertia', 'true')
.set('X-Inertia-Version', 'current-version')
expect(res.status).toBe(200)
expect(res.body).toHaveProperty('component')
expect(res.body).toHaveProperty('props')
expect(res.body).toHaveProperty('url')
})
})
Debug no navegador:
- Abra o DevTools > Network
- Filtre por X-Inertia header
- Inspecione o JSON de resposta para verificar component, props e version
8. Considerações Finais e Boas Práticas
Quando usar Inertia sem Laravel:
- Times que dominam Node.js, Python ou Go mas querem SPA sem API REST
- Projetos legados que migram para frontend moderno gradualmente
- Situações onde Laravel é overengineering para o backend necessário
Limitações importantes:
- Falta de ecossistema oficial (adapters comunitários podem quebrar)
- Manutenção manual do controle de versão de assets
- Ausência de recursos como defer props ou lazy loading nativos
Roteiro de migração de Laravel para outro stack:
1. Extraia a lógica de negócio do Laravel para um microserviço
2. Implemente o adapter Inertia no novo backend
3. Migre as rotas uma a uma, mantendo o frontend intacto
4. Substitua o Laravel por completo quando todas as rotas estiverem migradas
Para projetos que precisam de máxima produtividade, considere alternativas como HTMX (menos JavaScript) ou SPA puro com API REST (mais flexibilidade). Inertia sem Laravel é uma ponte elegante, mas exige cuidado com a manutenção do protocolo.
Referências
- Documentação oficial do Inertia.js — Guia completo do protocolo, incluindo especificações de headers e formato de resposta
- Inertia.js Adapter for Express — Implementação de referência do middleware Inertia para Node.js/Express
- Inertia.js Adapter for Flask — Adapter comunitário para Python/Flask com suporte a partial reloads
- Inertia.js Adapter for Go — Implementação mínima do protocolo Inertia para Go com Gin
- Tutorial: Inertia.js sem Laravel com Fastify — Artigo prático mostrando configuração completa com Fastify e React
- Inertia.js Protocol Specification — Especificação técnica detalhada do protocolo Inertia, útil para implementar adapters customizados
- Inertia.js Adapter for FastAPI — Adapter para Python/FastAPI com suporte a async e validação de versão