Un meta-framework ligero de React 19 con SSR, impulsado por Vite 8 + Hono.
Construye aplicaciones React full-stack con enrutamiento basado en archivos, renderizado del lado del servidor, generación estática de sitios y rutas API — configuración mínima, control máximo.
- Vite 8 — HMR instantáneo y builds rápidos con Rolldown
- React 19 — SSR con
renderToStringehydrateRoot - Enrutamiento basado en archivos — páginas, layouts anidados y rutas API desde el sistema de archivos
- SSR por defecto — cada página se renderiza en el servidor
- SSG — genera HTML estático con
generateStaticParams - Rutas API — basadas en archivos, con
createHandlerpara tipado de extremo a extremo - $fetch — cliente HTTP con body y respuesta tipados, con autocompletado de rutas
- Carga de datos — funciones
loadercon hidratación automática en el cliente - Navegación programática —
useNavigate()con soporte dereplacey View Transitions API - Revalidación de datos —
useRevalidate()para refrescar datos sin recargar la página - Guards de ruta — redirecciones del lado del servidor antes del renderizado
- SEO —
metadataygenerateMetadatapor página, con soporte de Open Graph y Twitter - Hooks de contexto —
useRequest(),useCtx(),useParams()accesibles desde cualquier función del handler - TypeScript primero — inferencia de tipos completa en todo el framework
npm install @devlusoft/devix react react-domRequiere React 19+, Vite 8+, Node 20+.
npx devix dev1. Crea devix.config.ts:
import { defineConfig } from '@devlusoft/devix/config'
export default defineConfig({
port: 3000,
})2. Crea tu primera página en app/pages/index.tsx:
export default function Home() {
return <h1>¡Hola devix!</h1>
}3. Ejecuta el servidor de desarrollo:
npx devix devapp/
├── pages/
│ ├── layout.tsx # Layout raíz (envuelve todas las páginas)
│ ├── index.tsx # → /
│ ├── about.tsx # → /about
│ └── blog/
│ ├── layout.tsx # Layout anidado (envuelve páginas de blog)
│ ├── index.tsx # → /blog
│ └── [slug].tsx # → /blog/:slug
└── api/
├── middleware.ts # Middleware global de la API
└── posts/
└── [id].ts # → GET/POST /api/posts/:id
import { useLoaderData } from '@devlusoft/devix'
import type { PageProps, LoaderContext } from '@devlusoft/devix'
export async function loader({ params, request }: LoaderContext) {
const post = await db.posts.findBySlug(params.slug)
return post
}
export default function BlogPost({ data, params }: PageProps<typeof loader>) {
return <article>{data.title}</article>
}export async function guard({ request }: LoaderContext) {
const user = await getSession(request)
if (!user) return '/login'
return null
}export const metadata = {
title: 'Inicio',
description: 'Bienvenido a mi sitio',
og: { image: '/og.png', type: 'website' },
twitter: { card: 'summary_large_image' },
}
// o dinámica:
export async function generateMetadata({ loaderData }) {
return { title: loaderData.title }
}import type { LayoutProps } from '@devlusoft/devix'
export default function RootLayout({ children }: LayoutProps) {
return (
<div>
<nav>...</nav>
{children}
</div>
)
}Pasa el tipo de params directamente sin necesidad de un loader:
import type { PageProps } from '@devlusoft/devix'
export default function ProviderPage({ params }: PageProps<{ providerId: string }>) {
return <h1>{params.providerId}</h1>
}import { useNavigate, useRevalidate } from '@devlusoft/devix'
function MyComponent() {
const navigate = useNavigate()
const revalidate = useRevalidate()
return (
<>
<button onClick={() => navigate('/dashboard')}>Ir al dashboard</button>
<button onClick={() => navigate('/login', { replace: true })}>Login (sin historial)</button>
<button onClick={() => navigate('/shop', { viewTransition: true })}>Con animación</button>
<button onClick={() => revalidate()}>Refrescar datos</button>
</>
)
}import { redirect } from '@devlusoft/devix'
export async function loader({ request }: LoaderContext) {
const user = await getSession(request)
if (!user) return redirect('/login', { replace: true })
return user
}createHandler da tipado de extremo a extremo — el body y el retorno se infieren automáticamente para $fetch:
import { createHandler, json } from '@devlusoft/devix'
export const GET = createHandler(async () => {
return json({ hello: 'world' })
})
export const POST = createHandler(async (body: { name: string }) => {
const item = await db.items.create(body)
return json(item, 201)
})
export const DELETE = createHandler(async () => null) // 204const res = await $fetch('/api/items', {
method: 'POST',
body: { name: 'nuevo item' },
})Configura output: 'static' y exporta generateStaticParams desde cualquier página dinámica:
// devix.config.ts
export default defineConfig({ output: 'static' })// app/pages/blog/[slug].tsx
export async function generateStaticParams() {
const posts = await db.posts.all()
return posts.map(p => ({ slug: p.slug }))
}npx devix generate # compila y pre-renderiza todas las páginas en dist/client/
npx devix start # sirve los archivos estáticos (sin SSR en runtime)| Comando | Descripción |
|---|---|
devix dev |
Inicia el servidor de desarrollo con HMR |
devix build |
Compila para producción |
devix start |
Inicia el servidor de producción |
devix generate |
Compila y pre-renderiza todas las páginas (SSG) |
// devix.config.ts
import { defineConfig } from '@devlusoft/devix/config'
export default defineConfig({
port: 3000, // puerto del servidor dev y producción (default: 3000)
host: false, // bind a 0.0.0.0 (default: false)
appDir: 'app', // directorio de la app (default: 'app')
publicDir: 'public', // directorio de archivos estáticos (default: 'public')
output: 'server', // 'server' | 'static' (default: 'server')
loaderTimeout: 10_000, // timeout de los loaders en ms (default: 10000)
css: ['./app/styles/global.css'], // archivos CSS globales
envPrefix: 'PUBLIC_', // expone variables de entorno con este prefijo al cliente
vite: {}, // extiende la configuración de Vite
})La documentación completa está en la carpeta docs/:
- Primeros pasos
- Enrutamiento
- Layouts
- Carga de datos
- Rutas API
- Metadata y SEO
- Generación estática (SSG)
- Configuración
MIT — devix es un proyecto de devlusoft.