Back to flin
flin

The Golden Rule: One .flin File Is All You Need

FLIN's golden rule: one .flin file replaces 15 config files. No package.json, no tsconfig, no webpack.

Thales & Claude | March 25, 2026 13 min flin
flinzero-configsimplicityone-filedeveloper-experience

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

One file.

This is FLIN's Golden Rule, and it is the single most radical departure from how modern web development works. In this article, we will build a complete, database-backed, fully reactive web application in a single .flin file -- and then show the equivalent in React/Next.js to make the contrast visceral.

The State of the Art: A React/Next.js Todo App

Before we show the FLIN version, let us be honest about what "building a todo app" means in 2024 with the industry-standard tools.

You begin by scaffolding the project:

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

Four commands. Three minutes if your internet connection is fast. Longer if it is not. When the dust settles, your project directory contains over 50,000 files and weighs approximately 400 megabytes.

You then need to create the database schema:

// 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 } ```

Run the migration:

npx prisma migrate dev --name init
npx prisma generate

Create the API route:

// 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 }); } ```

Create the delete/update API route:

// 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 }); } ```

Create the React component:

// 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([]); 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 (

My Todos

setNewTodo(e.target.value)} onKeyDown={e => e.key === 'Enter' && addTodo()} placeholder="What needs to be done?" />
    {filtered.map(todo => (
  • toggleTodo(todo)} /> {todo.title}
  • ))}
{todos.filter(t => !t.done).length} items left
); } ```

Let us count what this todo application required:

Files created or modified          Purpose
-------------------------------    ---------------------------
package.json                       Dependencies
prisma/schema.prisma               Database schema
.env                               DATABASE_URL
app/api/todos/route.ts             GET and POST endpoints
app/api/todos/[id]/route.ts        PATCH and DELETE endpoints
app/page.tsx                       React component

Six files for the application logic, plus fifteen configuration files generated by create-next-app. Over 120 lines of TypeScript. A Prisma migration. A SQLite database file. And approximately 400 MB of node_modules.

For a todo list.

The FLIN Version: One File

Here is the same application in 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) }

My Todos

{for todo in filtered}

{todo.title}
{/for}

{Todo.where(done == false).count} items left
```

Thirty-three lines. One file. Zero configuration. Zero dependencies. Zero megabytes of node_modules.

Save it as app.flin. Run flin dev. The application is live.

The Line-by-Line Comparison

Let us walk through what each section of the FLIN code replaces.

Lines 3-5: State management.

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

In React, this is useState called three times, with explicit type annotations, setter functions, and the overhead of understanding hooks rules. In FLIN, variables are declared and they are reactive. The compiler tracks dependencies.

This replaces: React hooks, Redux, Zustand, or any other state management library.

Lines 7-11: Entity definition.

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

This single block simultaneously defines: a TypeScript-like type, a database table with columns, an automatic migration, CRUD operations (find, where, all, count), and history tracking.

This replaces: a Prisma schema, a migration file, a database connection, and a Prisma client import.

Lines 13-17: Computed values.

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

The filtered variable recomputes automatically whenever filter changes. This is FLIN's reactive system at work -- no useMemo, no explicit dependency arrays, no manual subscription management.

This replaces: useMemo or useEffect with a dependency array, plus the Array.filter logic.

Line 21: The input with enter handler.

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

One line that does five things: binds the input value to newTodo, handles the Enter key event, creates a new Todo entity, saves it to the database, and clears the input field. In the React version, this is an onKeyDown handler that calls an async function that calls fetch that POSTs to an API route that calls Prisma.

This replaces: a React event handler, a fetch call, an API route, a Prisma create operation, and state update logic.

Lines 27-33: The todo list with checkbox and delete.

{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}

The checkbox handler toggles todo.done and saves the entity in a single expression. The delete button removes the entity from the database with the delete keyword. In React, each of these operations requires an async function, a fetch call, an API route, a Prisma operation, and state reconciliation.

This replaces: two React event handlers, two fetch calls, two API routes (PATCH and DELETE), two Prisma operations, and two state updates.

What the One-File Rule Actually Means

The Golden Rule is not "you must put everything in one file." FLIN supports multi-file projects. The rule is: one file must be sufficient.

The minimum viable FLIN application is always one file. You can grow from there:

// Stage 1: Everything in one file
app.flin

// Stage 2: Separate pages index.flin about.flin products/index.flin products/[id].flin

// Stage 3: Extract components components/Header.flin components/Footer.flin components/ProductCard.flin

// Stage 4: Separate entities (optional) entities/User.flin entities/Product.flin

// Stage 5: Add API routes api/users.flin api/users/[id].flin ```

At every stage, there are zero configuration files. The transition from Stage 1 to Stage 5 does not require adding a router library, a build configuration change, or a database migration. You just create more .flin files.

This is the critical difference from the JavaScript ecosystem, where every stage transition requires new tooling:

Stage                    JavaScript ecosystem adds
-----------------------  -----------------------------------
Single page              React + Vite config
Multiple pages           React Router or Next.js + its config
Components               (already available, but import/export needed)
Database                 Prisma + schema + migration + .env
API routes               Express or Next.js API routes + config
Production build         Docker + Dockerfile + build config

Each row adds configuration. Each row adds dependencies. Each row adds potential version conflicts. In FLIN, each row adds .flin files. Nothing else.

The Cognitive Cost of Configuration

Configuration files are not merely annoying. They impose a measurable cognitive cost.

Every configuration file in a project is a file that a developer must understand, or at least be aware of, to debug problems. When a build fails, the developer must check: is the error in my code, in the Vite config, in the TypeScript config, in the PostCSS config, or in some interaction between them?

This cognitive cost is invisible because it is constant. Like background noise, developers stop noticing it -- until it disappears.

Consider the debugging process when a CSS class does not apply in a Next.js project:

1. Is the class correct?                    (check your code)
2. Is Tailwind processing the file?         (check tailwind.config.js)
3. Is PostCSS running?                      (check postcss.config.js)
4. Is the CSS imported?                     (check globals.css import)
5. Is the dev server running the right config? (check next.config.js)
6. Is there a cache issue?                  (delete .next, restart)

Six places to check for a missing CSS class. In FLIN, the debugging process is:

1. Is the class correct?                    (check your code)

One place. Because there is one tool.

The Performance Argument

The one-file rule has direct performance implications.

A FLIN application starts in milliseconds because there is no module resolution, no dependency graph to construct, no configuration files to parse, no plugins to load. The FLIN compiler reads a .flin file, tokenizes it, parses it, type-checks it, and begins serving -- all in a fraction of the time it takes Vite to print its ASCII logo.

The production build is a single binary. No node_modules to ship. No package.json to specify an engine version. No Dockerfile to write. The binary runs on the target machine. That is the deployment story.

# 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 ```

The one-file rule cascades through the entire lifecycle. Simpler development. Simpler builds. Simpler deployment. Simpler debugging. Simpler onboarding. Every stage is simpler because the foundation is simpler.

An E-Commerce Example in One File

To prove that the one-file rule scales beyond toy examples, here is a product listing page with semantic search, filtering, and a shopping cart:

entity Product {
    name: text
    description: semantic text
    price: money
    stock: int
    category: text
    image: file
}

entity CartItem { product: Product quantity: int = 1 }

searchQuery = "" category = "all" cart = CartItem.all

products = if searchQuery.len > 3 then search searchQuery in Product by description limit 20 else if category != "all" then Product.where(category == category) else Product.all

cartTotal = cart.reduce(0, (sum, item) => sum + item.product.price * item.quantity)

Shop

{cart.count} items - {cartTotal}

{for product in products}

{product.name}

{product.price}

{product.stock} in stock

{/for}
```

Fifty-two lines. Two entities (Product and CartItem). Semantic search. Category filtering. A shopping cart with item count and total. All in one file, with zero configuration, zero dependencies, and zero build tools.

The React/Next.js equivalent would require: a Prisma schema with two models, a migration, at least three API routes (products list, product search, cart operations), a React component with multiple hooks, a state management solution for the cart, and an embedding pipeline for semantic search. Conservatively, 300-400 lines across 8-10 files.

The Golden Rule in Practice

The Golden Rule -- one .flin file is all you need -- is not a slogan. It is a design constraint that we enforce rigorously.

Every new feature proposed for FLIN must answer the question: "Does this still work in a single-file application?" If a feature requires creating a second file, a configuration file, or an external tool, it violates the Golden Rule.

This constraint has shaped FLIN in ways that no other design principle could:

  • Entities are inline. You define them in the same file where you use them. No separate schema file required.
  • Routes are file-based. The directory structure is the router. No router configuration file.
  • Types are inferred. No separate type definition file. No tsconfig.json.
  • Tests are inline. (When the test runner ships.) No separate test file required.
  • Styles can be inline. No separate CSS file required for simple cases.

The result is a language where the distance between idea and working application is one file and one command. Not five files and twelve commands. Not a twenty-minute scaffolding process. One file. One command.

app.flin and flin dev. That is the entire developer experience.

Like the elephant, FLIN carries everything it needs.

---

This is Part 5 of the "How We Built FLIN" series, documenting how a CEO in Abidjan and an AI CTO designed and built a programming language that replaces 47 technologies with one.

Series Navigation: - [1] Why We Built a Programming Language From Scratch - [2] 47 Technologies Replaced by One Language - [3] Naming a Language After an Elephant: The Fongbe Origin of FLIN - [4] Five Design Principles That Shape Every Line of FLIN - [5] The Golden Rule: One .flin File Is All You Need (you are here)

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles