Back to flin
flin

La Regla de Oro: un archivo .flin es todo lo que necesitas

La regla de oro de FLIN: un archivo .flin reemplaza 15 archivos de configuración. Sin package.json, sin tsconfig, sin webpack.

Thales & Claude | March 30, 2026 9 min flin
EN/ FR/ ES
flinrust

Sin package.json. Sin tsconfig.json. Sin webpack.config.js. Sin postcss.config.js. Sin .eslintrc. Sin .prettierrc. Sin next.config.js. Sin docker-compose.yml. Sin .env.local. Sin .env.production. Sin jest.config.js. Sin babel.config.js. Sin tailwind.config.js. Sin vite.config.ts.

Un archivo.

Esta es la Regla de Oro de FLIN, y es la desviación más radical de cómo funciona el desarrollo web moderno. En este artículo, construiremos una aplicación web completa, respaldada por base de datos y totalmente reactiva en un solo archivo .flin -- y luego mostraremos el equivalente en React/Next.js para hacer el contraste visceral.

El estado del arte: una aplicación de tareas React/Next.js

Antes de mostrar la versión FLIN, seamos honestos sobre lo que significa "construir una aplicación de tareas" en 2024 con las herramientas estándar de la industria.

Empiezas creando la estructura del proyecto:

bashnpx create-next-app@latest my-todo --typescript --tailwind --app --eslint
cd my-todo
npm install prisma @prisma/client
npx prisma init

Cuatro comandos. Tres minutos si tu conexión a internet es rápida. Más si no lo es. Cuando el polvo se asienta, tu directorio de proyecto contiene más de 50.000 archivos y pesa aproximadamente 400 megabytes.

Luego necesitas crear el esquema de la base de datos:

prisma// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

model Todo {
  id        Int      @id @default(autoincrement())
  title     String
  done      Boolean  @default(false)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

Ejecutar la migración:

bashnpx prisma migrate dev --name init
npx prisma generate

Crear la ruta de API:

typescript// app/api/todos/route.ts
import { PrismaClient } from '@prisma/client';
import { NextResponse } from 'next/server';

const prisma = new PrismaClient();

export async function GET() {
    const todos = await prisma.todo.findMany({
        orderBy: { createdAt: 'desc' }
    });
    return NextResponse.json(todos);
}

export async function POST(request: Request) {
    const body = await request.json();
    const todo = await prisma.todo.create({
        data: { title: body.title }
    });
    return NextResponse.json(todo, { status: 201 });
}

Crear la ruta de API para eliminar/actualizar:

typescript// app/api/todos/[id]/route.ts
import { PrismaClient } from '@prisma/client';
import { NextResponse } from 'next/server';

const prisma = new PrismaClient();

export async function PATCH(
    request: Request,
    { params }: { params: { id: string } }
) {
    const body = await request.json();
    const todo = await prisma.todo.update({
        where: { id: parseInt(params.id) },
        data: { done: body.done }
    });
    return NextResponse.json(todo);
}

export async function DELETE(
    request: Request,
    { params }: { params: { id: string } }
) {
    await prisma.todo.delete({
        where: { id: parseInt(params.id) }
    });
    return NextResponse.json({ success: true });
}

Crear el componente React:

tsx// app/page.tsx
'use client';

import { useState, useEffect } from 'react';

interface Todo {
    id: number;
    title: string;
    done: boolean;
}

export default function TodoApp() {
    const [todos, setTodos] = useState<Todo[]>([]);
    const [newTodo, setNewTodo] = useState('');
    const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('all');

    useEffect(() => {
        fetch('/api/todos')
            .then(res => res.json())
            .then(setTodos);
    }, []);

    const addTodo = async () => {
        if (!newTodo.trim()) return;
        const res = await fetch('/api/todos', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ title: newTodo })
        });
        const todo = await res.json();
        setTodos([todo, ...todos]);
        setNewTodo('');
    };

    const toggleTodo = async (todo: Todo) => {
        const res = await fetch(`/api/todos/${todo.id}`, {
            method: 'PATCH',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ done: !todo.done })
        });
        const updated = await res.json();
        setTodos(todos.map(t => t.id === updated.id ? updated : t));
    };

    const deleteTodo = async (id: number) => {
        await fetch(`/api/todos/${id}`, { method: 'DELETE' });
        setTodos(todos.filter(t => t.id !== id));
    };

    const filtered = todos.filter(todo => {
        if (filter === 'active') return !todo.done;
        if (filter === 'completed') return todo.done;
        return true;
    });

    return (
        <main className="max-w-md mx-auto p-4">
            <h1 className="text-2xl font-bold mb-4">My Todos</h1>
            <div className="flex gap-2 mb-4">
                <input
                    className="flex-1 border rounded px-3 py-2"
                    value={newTodo}
                    onChange={e => setNewTodo(e.target.value)}
                    onKeyDown={e => e.key === 'Enter' && addTodo()}
                    placeholder="What needs to be done?"
                />
            </div>
            <nav className="flex gap-2 mb-4">
                <button onClick={() => setFilter('all')}>All</button>
                <button onClick={() => setFilter('active')}>Active</button>
                <button onClick={() => setFilter('completed')}>Done</button>
            </nav>
            <ul>
                {filtered.map(todo => (
                    <li key={todo.id} className="flex items-center gap-2 py-1">
                        <input
                            type="checkbox"
                            checked={todo.done}
                            onChange={() => toggleTodo(todo)}
                        />
                        <span className={todo.done ? 'line-through' : ''}>
                            {todo.title}
                        </span>
                        <button onClick={() => deleteTodo(todo.id)}>x</button>
                    </li>
                ))}
            </ul>
            <footer className="mt-4 text-sm text-gray-500">
                {todos.filter(t => !t.done).length} items left
            </footer>
        </main>
    );
}

Contemos lo que esta aplicación de tareas requirió:

Archivos creados o modificados     Propósito
-------------------------------    ---------------------------
package.json                       Dependencias
prisma/schema.prisma               Esquema de base de datos
.env                               DATABASE_URL
app/api/todos/route.ts             Endpoints GET y POST
app/api/todos/[id]/route.ts        Endpoints PATCH y DELETE
app/page.tsx                       Componente React

Seis archivos para la lógica de la aplicación, más quince archivos de configuración generados por create-next-app. Más de 120 líneas de TypeScript. Una migración de Prisma. Un archivo de base de datos SQLite. Y aproximadamente 400 MB de node_modules.

Para una lista de tareas.

La versión FLIN: un archivo

Aquí está la misma aplicación en FLIN:

flin// app.flin -- Complete todo application with database persistence

todos = []
filter = "all"
newTodo = ""

entity Todo {
    title: text
    done: bool = false
    created: time = now
}

filtered = match filter {
    "all"       -> Todo.all
    "active"    -> Todo.where(done == false)
    "completed" -> Todo.where(done == true)
}

<main>
    <h1>My Todos</h1>

    <input value={newTodo} placeholder="What needs to be done?"
           enter={save Todo { title: newTodo }; newTodo = ""}>

    <nav>
        <button click={filter = "all"}>All</button>
        <button click={filter = "active"}>Active</button>
        <button click={filter = "completed"}>Done</button>
    </nav>

    {for todo in filtered}
        <div class="todo-item">
            <input type="checkbox" checked={todo.done}
                   change={todo.done = !todo.done; save todo}>
            <span class={if todo.done then "done" else ""}>{todo.title}</span>
            <button click={delete todo}>x</button>
        </div>
    {/for}

    <footer>{Todo.where(done == false).count} items left</footer>
</main>

Treinta y tres líneas. Un archivo. Cero configuración. Cero dependencias. Cero megabytes de node_modules.

Guárdalo como app.flin. Ejecuta flin dev. La aplicación está en funcionamiento.

Lo que la regla de un archivo realmente significa

La Regla de Oro no es "debes poner todo en un archivo." FLIN soporta proyectos multi-archivo. La regla es: un archivo debe ser suficiente.

La aplicación FLIN mínima viable es siempre un archivo. Puedes crecer desde ahí:

// Etapa 1: Todo en un archivo
app.flin

// Etapa 2: Páginas separadas
index.flin
about.flin
products/index.flin
products/[id].flin

// Etapa 3: Extraer componentes
components/Header.flin
components/Footer.flin
components/ProductCard.flin

// Etapa 4: Entidades separadas (opcional)
entities/User.flin
entities/Product.flin

// Etapa 5: Añadir rutas de API
api/users.flin
api/users/[id].flin

En cada etapa, hay cero archivos de configuración. La transición de la Etapa 1 a la Etapa 5 no requiere añadir una biblioteca de enrutamiento, un cambio de configuración de compilación o una migración de base de datos. Simplemente creas más archivos .flin.

El coste cognitivo de la configuración

Los archivos de configuración no son meramente molestos. Imponen un coste cognitivo medible.

Cada archivo de configuración en un proyecto es un archivo que un desarrollador debe entender, o al menos conocer, para depurar problemas. Cuando una compilación falla, el desarrollador debe verificar: ¿el error está en mi código, en la configuración de Vite, en la configuración de TypeScript, en la configuración de PostCSS, o en alguna interacción entre ellas?

Considera el proceso de depuración cuando una clase CSS no se aplica en un proyecto Next.js:

1. ¿Es correcta la clase?                    (revisa tu código)
2. ¿Tailwind está procesando el archivo?      (revisa tailwind.config.js)
3. ¿PostCSS está ejecutándose?                (revisa postcss.config.js)
4. ¿Está importado el CSS?                    (revisa importación globals.css)
5. ¿El servidor dev ejecuta la config correcta? (revisa next.config.js)
6. ¿Hay un problema de caché?                (elimina .next, reinicia)

Seis lugares para revisar por una clase CSS faltante. En FLIN, el proceso de depuración es:

1. ¿Es correcta la clase?                    (revisa tu código)

Un lugar. Porque hay una herramienta.

El argumento de rendimiento

La regla de un archivo tiene implicaciones directas de rendimiento.

Una aplicación FLIN arranca en milisegundos porque no hay resolución de módulos, no hay grafo de dependencias que construir, no hay archivos de configuración que parsear, no hay plugins que cargar. El compilador de FLIN lee un archivo .flin, lo tokeniza, lo parsea, lo verifica con tipos y comienza a servir -- todo en una fracción del tiempo que le toma a Vite imprimir su logo ASCII.

La compilación de producción es un binario único. Sin node_modules que enviar. Sin package.json para especificar una versión del motor. Sin Dockerfile que escribir. El binario se ejecuta en la máquina destino. Esa es la historia de despliegue.

bash# FLIN deployment
flin build
scp app /server:/usr/local/bin/
ssh server 'app'

# Next.js deployment
npm run build
# ... configure Docker
# ... write Dockerfile
# ... build image
# ... push to registry
# ... configure orchestrator
# ... deploy

La regla de un archivo se propaga por todo el ciclo de vida. Desarrollo más simple. Compilaciones más simples. Despliegue más simple. Depuración más simple. Incorporación más simple. Cada etapa es más simple porque la base es más simple.

La Regla de Oro en la práctica

La Regla de Oro -- un archivo .flin es todo lo que necesitas -- no es un eslogan. Es una restricción de diseño que aplicamos rigurosamente.

Cada nueva funcionalidad propuesta para FLIN debe responder a la pregunta: "¿Esto sigue funcionando en una aplicación de un solo archivo?" Si una funcionalidad requiere crear un segundo archivo, un archivo de configuración o una herramienta externa, viola la Regla de Oro.

Esta restricción ha moldeado FLIN de formas que ningún otro principio de diseño podría:

  • Las entidades son inline. Las defines en el mismo archivo donde las usas. Sin archivo de esquema separado requerido.
  • Las rutas son basadas en archivos. La estructura de directorios es el enrutador. Sin archivo de configuración de enrutador.
  • Los tipos se infieren. Sin archivo de definición de tipos separado. Sin tsconfig.json.
  • Las pruebas son inline. (Cuando el ejecutor de pruebas se lance.) Sin archivo de prueba separado requerido.
  • Los estilos pueden ser inline. Sin archivo CSS separado requerido para casos simples.

El resultado es un lenguaje donde la distancia entre la idea y la aplicación funcional es un archivo y un comando. No cinco archivos y doce comandos. No un proceso de andamiaje de veinte minutos. Un archivo. Un comando.

app.flin y flin dev. Esa es toda la experiencia de desarrollador.

Como el elefante, FLIN lleva todo lo que necesita.


Esta es la Parte 5 de la serie "Cómo construimos FLIN", que documenta cómo un CEO en Abiyán y un CTO IA diseñaron y construyeron un lenguaje de programación que reemplaza 47 tecnologías por una sola.

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles