---
title: "FastHTML para Principiantes: Crea una UI para tu App en Python"
description: "Aprende FastHTML desde cero y crea tu primera interfaz para una app en Python en 5 minutos usando solo Python y HTMX. Sin JavaScript ni herramientas de compilación."
date: 2025-02-26
categories: ["web-development"]
tags: ["fasthtml","python","web-development"]
---

import Notice from "@components/widgets/Notice.astro";
import ListCheck from "@components/widgets/ListCheck.astro";
import Accordion from "@components/widgets/Accordion.astro";
import Tabs from "@components/widgets/Tabs.astro";
import Tab from "@components/widgets/Tab.astro";
import YouTubeEmbed from "@components/widgets/YouTubeEmbed.astro";

[FastHTML](https://fastht.ml/) es un framework web en Python creado por Jeremy Howard y [Answer.AI](https://www.answer.ai/) que te permite construir aplicaciones web completas usando solo Python. Sin JavaScript, sin herramientas de compilación, sin lenguajes de plantillas. Si alguna vez quisiste crear una interfaz para tu app en Python pero no querías aprender React, Vue o incluso HTML/CSS desde cero, este tutorial de FastHTML para principiantes te llevará hasta allí en minutos.

FastHTML funciona mapeando funciones de Python directamente a elementos HTML. Escribes `P("Hello")` en lugar de `<p>Hello</p>`. Incluye HTMX 2.x para la interactividad y Pico CSS para el estilo predeterminado, ambos integrados sin configuración adicional. El resultado: pasas de un script de Python a una app web funcional con una UI interactiva más rápido que con cualquier otro framework web en Python.

<ListCheck>
  <ul>
    <li>Instalar FastHTML con pip o uv</li>
    <li>Entender la sintaxis de función a HTML</li>
    <li>Construir tu primera página web con un servidor funcional</li>
    <li>Crear componentes de UI comunes (formularios, tablas, barras de navegación)</li>
    <li>Añadir interactividad con HTMX, sin necesidad de JavaScript</li>
    <li>Construir una aplicación de tareas completa</li>
    <li>Solucionar errores comunes</li>
    <li>Ejecutar tu app de FastHTML en producción en un VPS</li>
  </ul>
</ListCheck>

Si estás explorando frameworks de UI en Python en general, también echa un vistazo a [nuestro tutorial de NiceGUI para principiantes](/es/empezar-nicegui/), otra opción para construir interfaces impulsadas por Python.

## ¿Por qué FastHTML?

El desarrollo web tradicional te obliga a hacer malabares con HTML, CSS, JavaScript y frameworks como React o Vue solo para poner un botón en pantalla. FastHTML elimina todo eso manteniendo todo en Python. Esto es lo que lo hace merecer una oportunidad:

- **Un solo lenguaje**: backend y frontend en Python, sin cambio de contexto
- **HTMX primero**: interactividad sin escribir JavaScript. HTMX 2.x viene incluido de serie
- **Pico CSS incluido**: tus páginas se ven bien desde el primer momento con `fast_app()`, sin necesidad del CDN de Tailwind
- **Sin herramientas de compilación**: sin npm, sin webpack, sin node_modules. Solo son archivos de Python
- **Sintaxis nativa de Python**: funciones familiares de Python en lugar de lenguajes de plantillas
- **Construido sobre Starlette/ASGI**: base lista para producción, se integra en cualquier host ASGI

<Notice type="info" title="Valores predeterminados del framework">
  `fast_app()` de FastHTML incluye Pico CSS y recarga en vivo de forma predeterminada. HTMX 2.x (>=2.0.4) viene integrado, sin necesidad de importación separada ni enlace a CDN. Obtienes una página con estilo e interactiva con configuración cero.
</Notice>

FastHTML va por un carril distinto al de Django o Flask. Esos son excelentes frameworks de backend, pero construir una UI con ellos implica escribir plantillas HTML o combinarlos con un frontend en JavaScript. FastHTML te lo da todo en Python. Puedes [comparar FastHTML con Django, Flask y FastAPI en nuestra guía de frameworks web de Python](/best-python-web-frameworks/), o consultar [Streamlit vs. NiceGUI](/es/streamlit-vs-nicegui/) para otros enfoques de UI en Python.

<YouTubeEmbed
  url="https://www.youtube.com/embed/fqzTg5CrlGY"
  label="FastHTML para Principiantes"
/>

## Serie FastHTML

A continuación están los artículos de esta serie para ayudarte a construir con FastHTML:

- [FastHTML: primeros pasos](/es/empezar-fasthtml/), este artículo
- [FastHTML con múltiples páginas](/es/fasthtml-multiples-paginas/)
- [Herramientas de IA complejas con FastHTML](/es/fasthtml-herramientas-ia-complejas/)
- [Construir una app web simple con IA usando FastHTML y Pydantic AI](/es/fasthtml-herramientas-pydanticai/)
- [Añadir historial con base de datos SQLite a tu generador de títulos con IA en FastHTML](/es/fasthtml-sqlite-db/)
- [Autenticación en FastHTML](/es/fasthtml-autenticacion-usuarios/)

## Instalación de FastHTML

### Requisitos previos

- **Python 3.10 o superior** (se recomienda 3.11/3.12). FastHTML no funciona con Python 3.9 o inferior.
- Acceso a la terminal en cualquier SO (macOS, Linux, Windows)

Si necesitas instalar o actualizar Python en macOS, consulta nuestra guía sobre [cómo instalar Python en macOS](/install-upgrade-python-mac/). Para otros sistemas operativos, visita [python.org](https://www.python.org/downloads/).

<Tabs>
  <Tab name="pip (predeterminado)">
    Crea un entorno virtual e instala FastHTML:

    ```sh
    python3 -m venv fhenv
    source fhenv/bin/activate
    # On Windows:
    # fhenv\Scripts\activate
    ```

    Esto crea un entorno aislado para tu proyecto, evitando conflictos de paquetes con el Python de tu sistema.

    ```bash
    pip install python-fasthtml
    ```

    Esto descarga el paquete FastHTML y sus dependencias. También puedes consultar la [documentación oficial de FastHTML](https://www.fastht.ml/docs) para más detalles.
  </Tab>
  <Tab name="uv (más rápido)">
    [`uv`](https://github.com/astral-sh/uv) es un gestor de paquetes de Python rápido escrito en Rust. Si no lo has usado antes, consulta nuestra guía sobre [primeros pasos con uv](/es/empezar-uv/).

    ```sh
    uv init fh-app
    cd fh-app
    uv add python-fasthtml
    ```

    Para ejecutar tu app:

    ```sh
    uv run main.py
    ```

    `uv` resuelve e instala dependencias significativamente más rápido que pip y gestiona los entornos virtuales automáticamente.
  </Tab>
</Tabs>

<Notice type="info" title="¿ModuleNotFoundError?">
  Si ves `ModuleNotFoundError: No module named 'fasthtml'`, casi siempre significa que tu versión de Python es inferior a 3.10 o que olvidaste activar tu entorno virtual. Compruébalo con `python3 --version` y asegúrate de que `which python3` apunte a tu venv.
</Notice>

## Entendiendo la sintaxis de FastHTML

La característica destacada de FastHTML es su capacidad de permitirte escribir páginas web usando funciones de Python que se mapean directamente a etiquetas HTML. En lugar de hacer malabares con archivos HTML separados, defines la estructura de tu página en Python:

```python
from fasthtml.common import *

# Creating a paragraph element
paragraph = P("Hello, World!")
```

`P` es una función de FastHTML que genera `<p>Hello, World!</p>` al renderizarse. FastHTML proporciona funciones similares para todos los elementos HTML estándar:

| HTML | FastHTML | Ejemplo |
|------|----------|---------|
| `<p>` | `P()` | `P("Text")` |
| `<h1>` | `H1()` | `H1("Heading")` |
| `<div>` | `Div()` | `Div(P("Child element"))` |
| `<a>` | `A()` | `A("Link text", href="https://example.com")` |
| `<input>` | `Input()` | `Input(type="text", name="username")` |

**Cómo funcionan las funciones de FastHTML:**

Cada función acepta:
- **Argumentos posicionales** para elementos hijos o contenido
- **Argumentos con nombre** para atributos HTML

Por ejemplo, `P("Hello", cls="greeting")` produce `<p class="greeting">Hello</p>`.

FastHTML mapea dos palabras clave reservadas de Python para evitar errores de sintaxis:
- `class` → `cls` (ya que `class` es una palabra clave de Python)
- `for` → `fr` (ya que `for` es una palabra clave de Python)

El import comodín `from fasthtml.common import *` es intencional y está explícitamente respaldado por el equipo de FastHTML. Incorpora todos los componentes estándar, helpers y la función `serve()` que necesitas.

## Construyendo tu primera página web

Vamos a crear una página web simple. Crea un archivo llamado `main.py`:

```python
from fasthtml.common import *

app, rt = fast_app()

@rt("/")
def get():
    return Titled("FastHTML Basics",
        P("Below is a list of features:"),
        Ul(
            Li("Easy to learn"),
            Li("Python-based"),
            Li("Dynamic and responsive")
        )
    )

serve()
```

Vamos a desglosarlo:

1. `from fasthtml.common import *` importa todos los componentes y helpers de FastHTML
2. `app, rt = fast_app()` crea la aplicación y devuelve un decorador de rutas. `fast_app()` aplica valores predeterminados útiles: estilos de Pico CSS, recarga en vivo y HTMX 2.x, todo incluido automáticamente
3. `@rt("/")` mapea la función a la URL raíz. El nombre de la función `get` le indica a FastHTML que maneje peticiones GET
4. `Titled()` es un helper que genera tanto una etiqueta `<title>` como un `<h1>` en una sola llamada
5. `serve()` inicia el servidor de FastHTML (comprueba `__name__` internamente, así que no necesitas el guard `if __name__ == "__main__":`)

<Notice type="info" title="La forma larga sigue funcionando">
  A veces verás `app = FastHTML()` con `@app.get("/")` en ejemplos antiguos. Eso sigue funcionando, pero `fast_app()` es el patrón recomendado actualmente. Es lo que usan la documentación oficial y todos los ejemplos recientes.
</Notice>

### Verifica que funciona

Ejecuta la app desde tu terminal:

```bash
python main.py
```

Deberías ver una salida como:

```
INFO:     Uvicorn running on http://0.0.0.0:5001 (Press CTRL+C to quit)
INFO:     Started reloader process ... using WatchFiles
INFO:     Started server process ...
```

Abre tu navegador en `http://localhost:5001`. Deberías ver una página con un encabezado y una lista de viñetas.

También puedes hacer una prueba rápida desde la CLI:

```bash
curl -s localhost:5001 | head
```

Si ves salida HTML, la app está sirviendo correctamente.

<Notice type="success" title="Está funcionando">
  El navegador muestra tu página con los estilos de Pico CSS aplicados automáticamente. Sin CDN de Tailwind, sin archivo CSS, `fast_app()` se encarga de todo. El recargador en vivo también detecta los cambios de código sin reiniciar manualmente.
</Notice>

**Qué ocurre detrás de escena:**

1. FastHTML recibe la petición para `/`
2. Llama a la función `get()`
3. La función devuelve una página construida con componentes de FastHTML
4. FastHTML convierte estos componentes a HTML y los envía al navegador

### Personalizando los encabezados (CSS/JS)

Pico CSS es el predeterminado con `fast_app()`. Si prefieres Tailwind o necesitas CSS/JS personalizado, pásalos mediante el parámetro `hdrs`:

```python
app, rt = fast_app(
    pico=False,
    hdrs=(Script(src="https://cdn.tailwindcss.com"),)
)
```

Establece `pico=False` para desactivar el Pico CSS predeterminado, luego añade lo que necesites en `hdrs`.

## Componentes básicos de UI

Exploremos los componentes fundamentales para construir interfaces con FastHTML.

### Elementos de texto

Los elementos de texto son la base de cualquier interfaz:

```python
@rt("/text-elements")
def get():
    return Titled("Text Elements",
        H2("Subheading"),
        H3("Section heading"),
        P("This is a regular paragraph with some ",
          Strong("bold text"), " and some ",
          Em("italicized text"), " mixed in."),
        P("You can also use ", Code("code snippets"), " inline.")
    )
```

`Strong` renderiza `<strong>`, `Em` renderiza `<em>`, y `Code` renderiza `<code>`. FastHTML sigue la convención de nombres de los elementos HTML. Si conoces HTML, ya conoces FastHTML.

### Contenedores y layout

`Div` es el componente principal para estructurar páginas:

```python
@rt("/layout")
def get():
    return Titled("Layout Demo",
        # Sidebar and main content
        Div(
            Div(
                H2("Sidebar"),
                Ul(Li("Home"), Li("About"), Li("Services")),
            ),
            Div(
                H2("Main Content"),
                P("This is the main content area."),
            ),
        )
    )
```

Con Pico CSS como predeterminado, los contenedores obtienen un espaciado y una tipografía sensatos sin clases extra. Si necesitas layouts personalizados (grids, flexbox), añade tu propio CSS mediante `hdrs` o usa atributos `style` en línea.

### Enlaces y botones

Los enlaces usan `A()` y los botones usan `Button()`:

```python
@rt("/navigation")
def get():
    return Titled("Navigation Demo",
        Nav(
            A("Home", href="/"),
            A("Features", href="/features"),
            A("Docs", href="/docs"),
            Button("Login"),
        ),
        P("Welcome to FastHTML")
    )
```

`Nav` crea un elemento `<nav>`. Con Pico CSS, las barras de navegación y los botones obtienen un estilo básico automáticamente.

### Formularios e inputs

Los formularios permiten a los usuarios enviar datos a tu app. Aquí hay un formulario de contacto:

```python
@rt("/contact")
def get():
    return Titled("Contact Us",
        Form(
            Label("Name", Input(type="text", name="name", placeholder="Your name")),
            Label("Email", Input(type="email", name="email", placeholder="Your email")),
            Label("Message", Textarea(name="message", placeholder="Your message", rows=5)),
            Button("Send Message", type="submit"),
            action="/submit-contact",
            method="post"
        )
    )
```

Observa el patrón de `Label` — el input está anidado dentro del label, que es el enfoque preferido en la documentación. Si necesitas un atributo `for` explícito, usa `fr` (no `For`):

```python
Label("Name:", fr="name")
Input(type="text", id="name", name="name")
```

FastHTML puede inyectar los valores de los campos del formulario directamente en tu manejador mediante parámetros tipados:

```python
@rt("/submit-contact")
def post(name: str, email: str, message: str):
    return P(f"Thanks {name}, we got your message.")
```

Cuando el formulario se envía, FastHTML extrae `name`, `email` y `message` de los datos del formulario y los pasa como argumentos de la función — sin necesidad de parseo manual.

<Notice type="warning" title="Manejador ausente = 404">
  El formulario se envía a `/submit-contact`. Asegúrate de definir un manejador POST correspondiente (`@rt("/submit-contact") def post(...)`) o el formulario devolverá un error 404.
</Notice>

### Listas y tablas

Organizando datos con listas y tablas:

```python
@rt("/data-table")
def get():
    return Titled("User Data",
        Table(
            Thead(
                Tr(Th("ID"), Th("Name"), Th("Email"), Th("Role"))
            ),
            Tbody(
                Tr(Td("1"), Td("John Doe"), Td("john@example.com"), Td("Admin")),
                Tr(Td("2"), Td("Jane Smith"), Td("jane@example.com"), Td("User")),
                Tr(Td("3"), Td("Robert Johnson"), Td("robert@example.com"), Td("Editor")),
            )
        )
    )
```

Pico CSS estila automáticamente las tablas con bordes, padding y colores de fila alternos — sin necesidad de clases extra.

## Añadiendo interactividad con HTMX

FastHTML integra [HTMX](https://htmx.org/) — una biblioteca que te permite crear UIs dinámicas sin escribir JavaScript. FastHTML incluye HTMX 2.x (≥2.0.4) de serie, así que no hay nada extra que instalar.

<Notice type="info" title="HTMX 2.x incluido">
  FastHTML incluye HTMX 2.x de fábrica. Sin enlace a CDN ni importación separada. Los atributos `hx_*` (los guiones bajos se convierten en guiones en el HTML) están todos disponibles como argumentos con nombre.
</Notice>

Aquí hay un ejemplo de contador que se actualiza sin recargar la página completa:

```python
from fasthtml.common import *

app, rt = fast_app()

counter = 0

@rt("/")
def get():
    return Titled("HTMX Counter",
        Div(
            P(f"Current count: {counter}", id="counter"),
            Button("Increment",
                  hx_post="/increment",
                  hx_target="#counter"),
            Button("Decrement",
                  hx_post="/decrement",
                  hx_target="#counter"),
        )
    )

@rt("/increment")
def post():
    global counter
    counter += 1
    return P(f"Current count: {counter}", id="counter")

@rt("/decrement")
def post():
    global counter
    counter -= 1
    return P(f"Current count: {counter}", id="counter")

serve()
```

Ejecútalo, haz clic en Increment y observa cómo el contador se actualiza — sin recarga completa de página. Eso es HTMX en acción.

### Referencia de atributos HTMX

FastHTML proporciona los atributos HTMX como argumentos con nombre de Python:

| Atributo | Qué hace | Ejemplo |
|-----------|-------------|---------|
| `hx_post` | Envía una petición POST al hacer clic | `hx_post="/increment"` |
| `hx_get` | Envía una petición GET al hacer clic | `hx_get="/items"` |
| `hx_target` | Qué elemento actualizar (selector CSS) | `hx_target="#counter"` |
| `hx_swap` | Cómo intercambiar la respuesta (innerHTML, outerHTML, beforeend) | `hx_swap="outerHTML"` |
| `hx_trigger` | Cuándo disparar (click, change, etc.) | `hx_trigger="change"` |

Los guiones bajos en los nombres de atributos se convierten en guiones en el HTML renderizado: `hx_post` → `hx-post`.

## Construyendo una aplicación de tareas

Combinemos todo en una app de tareas funcional con añadir, marcar y eliminar:

```python
from fasthtml.common import *
from dataclasses import dataclass

app, rt = fast_app()

todos = []
todo_id_counter = 0

@dataclass
class Todo:
    id: int
    title: str
    completed: bool = False

@rt("/")
def get():
    return Titled("Todo App",
        Form(
            Div(
                Input(type="text", name="title", placeholder="Add a new todo"),
                Button("Add", type="submit"),
            ),
            hx_post="/add-todo",
            hx_target="#todo-list",
            hx_swap="beforeend"
        ),
        Div(id="todo-list"),
    )

@rt("/add-todo")
def post(title: str):
    global todo_id_counter
    if not title.strip():
        return ""
    todo_id_counter += 1
    new_todo = Todo(id=todo_id_counter, title=title)
    todos.append(new_todo)
    return create_todo_item(new_todo)

@rt("/toggle-todo/{id}")
def post(id: int):
    for todo in todos:
        if todo.id == id:
            todo.completed = not todo.completed
            return create_todo_item(todo)
    return ""

@rt("/delete-todo/{id}")
def delete(id: int):
    global todos
    todos = [t for t in todos if t.id != id]
    return ""

def create_todo_item(todo: Todo):
    return Div(
        Div(
            Input(type="checkbox",
                  checked=todo.completed,
                  hx_post=f"/toggle-todo/{todo.id}",
                  hx_target=f"#todo-{todo.id}",
                  hx_swap="outerHTML"),
            Span(todo.title,
                 style="text-decoration: line-through; opacity: 0.6" if todo.completed else ""),
        ),
        Button("×",
               hx_delete=f"/delete-todo/{todo.id}",
               hx_target=f"#todo-{todo.id}",
               hx_swap="outerHTML"),
        id=f"todo-{todo.id}",
    )

serve()
```

### Cómo funciona la app de tareas

1. **Estructura de datos**: un dataclass de Python define la forma del elemento de tarea
2. **Añadir**: el formulario envía un POST a `/add-todo` — FastHTML extrae `title` de los datos del formulario automáticamente. HTMX añade el nuevo elemento a la lista
3. **Marcar**: hacer clic en el checkbox envía un POST a `/toggle-todo/{id}` — el servidor invierte `completed` y devuelve el HTML actualizado. HTMX lo intercambia en su lugar
4. **Eliminar**: el botón × envía una petición DELETE a `/delete-todo/{id}` — el servidor elimina el elemento y devuelve HTML vacío, que HTMX usa para quitar el elemento de la página

### De memoria a almacenamiento persistente

La lista `todos = []` se reinicia cada vez que el servidor se reinicia. Está bien para aprender, pero no para nada real. FastHTML incluye [Fastlite](https://github.com/AnswerDotAI/fasthtml#fastlite) para la persistencia con SQLite:

```python
from fastlite import *

db = database("todos.db")
todos = db.create(Todo, pk="id", transform=True)
```

Desde la versión 0.14.1, `fastlite` debe importarse explícitamente (ya no se re-exporta desde `fasthtml.common`). Esto te da un almacén respaldado por SQLite que sobrevive a los reinicios.

Para el tutorial completo, consulta [cómo añadir una base de datos SQLite a FastHTML](/es/fasthtml-sqlite-db/).

<Notice type="warning" title="Estado solo para desarrollo">
  El estado global mutable como `counter` y `todos` se rompe si ejecutas más de un proceso worker. Usa `session` o una base de datos para cualquier cosa más allá de un prototipo.
</Notice>

## Novedades de FastHTML desde 2025

FastHTML ha ganado funciones significativas desde que este artículo se publicó por primera vez. Esto es lo que vale la pena conocer:

- **[MonsterUI](https://www.answer.ai/posts/2025-01-15-monsterui.html)** — una biblioteca de componentes estilo shadcn construida sobre FastHTML por Answer.AI. El siguiente paso recomendado después de aprender lo básico.
- **Fastlite** — persistencia SQLite mediante `fastlite` (ver [añadir una base de datos SQLite a FastHTML](/es/fasthtml-sqlite-db/))
- **Sessions** — añade un parámetro `session` a cualquier manejador para estado por usuario
- **Beforeware** — guardias de autenticación mediante `fast_app(before=Beforeware(...))`
- **OAuth integrado** — soporte para Google, GitHub, Apple, Auth0 (ver [Autenticación en FastHTML](/es/fasthtml-autenticacion-usuarios/))
- **Toasts** — `setup_toasts(app)` + `add_toast(session, msg, "success")` para mensajes flash
- **APIRouter** — divide las rutas en múltiples archivos para apps más grandes (ver [FastHTML con múltiples páginas](/es/fasthtml-multiples-paginas/))
- **WebSockets y SSE** — `exts='ws'` para soporte de WebSocket, `EventStream` para eventos enviados por el servidor
- **`fill_form()`** — vincula instancias de dataclass a campos de formulario automáticamente
- **llms.txt / llms-ctx.txt** — documentación amigable para LLMs en [fastht.ml](https://www.fastht.ml/) para asistentes de código con IA
- **FastHTML Gallery** — [gallery.fastht.ml](https://gallery.fastht.ml/) para ejemplos e inspiración

## La ventaja de FastHTML

El enfoque de FastHTML tiene algunas ventajas claras:

1. **UI impulsada por Python**: escribe backend y frontend en un solo lenguaje
2. **Sintaxis declarativa**: compone funciones en lugar de escribir plantillas HTML
3. **Interactividad integrada**: HTMX viene incluido — no se necesita framework de JavaScript
4. **Sin cambio de contexto**: permanece en Python desde la capa de datos hasta la UI
5. **Dependencias mínimas**: sin Node.js, sin npm, sin paso de compilación
6. **Iteración rápida**: UIs funcionales en minutos

FastHTML es adecuado para:
- Herramientas internas y paneles
- Prototipos y MVPs
- Aplicaciones de visualización de datos
- Interfaces de administración
- Cualquier app donde la velocidad de desarrollo importa más que las interacciones complejas del lado del cliente

Si estás comparando enfoques de UI en Python, echa un vistazo a [Streamlit vs. NiceGUI](/es/streamlit-vs-nicegui/) — otros dos frameworks en este espacio.

## Solución de problemas comunes

<Accordion label="ModuleNotFoundError: No module named 'fasthtml'" group="troubleshooting" expanded="true">
  **Causa**: Casi siempre una de dos cosas — tu versión de Python es inferior a 3.10, u olvidaste activar tu entorno virtual.

  **Solución**:
  ```sh
  python3 --version
  # Should show 3.10 or higher

  which python3
  # Should point to your venv, e.g. /path/to/fhenv/bin/python3
  ```

  Si Python es demasiado viejo, actualízalo. Si `which python3` no apunta a tu venv, ejecuta `source fhenv/bin/activate` de nuevo.
</Accordion>

<Accordion label="El puerto 5001 ya está en uso" group="troubleshooting">
  **Causa**: Otro proceso está usando el puerto 5001.

  **Solución**: Cambia el puerto pasándolo a `serve()`:
  ```python
  serve(port=8000)
  ```

  Desde la versión 0.12.42, `serve()` pasa los argumentos con nombre directamente a `uvicorn.run`, así que cualquier opción de uvicorn funciona.
</Accordion>

<Accordion label="Bucle infinito de recarga en vivo" group="troubleshooting">
  **Causa**: Incompatibilidad conocida con uvicorn ≥0.39.

  **Solución**: Actualiza FastHTML — esto se corrigió en la versión 0.12.39:
  ```sh
  pip install --upgrade python-fasthtml
  ```
</Accordion>

<Accordion label="El formulario devuelve 404 al enviarse" group="troubleshooting">
  **Causa**: La URL de `action` de tu formulario no coincide con ningún manejador, o el método HTTP es incorrecto.

  **Solución**: Asegúrate de tener un manejador que coincida tanto con la URL como con el método. Un formulario con `action="/submit-contact"` y `method="post"` necesita:
  ```python
  @rt("/submit-contact")
  def post(name: str, email: str, message: str):
      return P(f"Thanks {name}, we got your message.")
  ```
  Verifica que el nombre de la función coincida con el método HTTP (`get`, `post`, `delete`, etc.).
</Accordion>

## Ejecutando FastHTML en producción

`serve()` de FastHTML es un envoltorio de conveniencia para desarrollo alrededor de uvicorn. Para producción, hay algunas cosas que debes saber.

### De `serve()` a un servidor ASGI de producción

Ejecuta con uvicorn directamente para producción:

```sh
uvicorn main:app --host 0.0.0.0 --port 8000
```

O usa gunicorn con workers de uvicorn para configuraciones multi-proceso:

```sh
gunicorn main:app --worker-class uvicorn.workers.UvicornWorker --workers 2 --bind 0.0.0.0:8000
```

<Notice type="warning" title="Nunca despliegues con debug=True">
  `debug=True` expone páginas de error detalladas en el navegador. Bien para desarrollo, un riesgo de seguridad en producción. Quítalo o ponlo en `False` antes de desplegar.
</Notice>

### Despliega detrás de un proxy inverso

Ejecuta tu app de FastHTML detrás de Caddy, Nginx o Traefik para la terminación TLS, servir archivos estáticos y limitación de tasa. Un Caddyfile básico:

```
yourdomain.com {
    reverse_proxy localhost:8000
}
```

Caddy gestiona los certificados HTTPS automáticamente. La configuración TLS sin configuración es difícil de superar para quienes operan en solitario.

FastHTML es Starlette/ASGI puro — funciona con cualquier host compatible con ASGI. El estado vive en el proceso o en un archivo SQLite, así que el coste de infraestructura en la nube es cero de forma predeterminada. Solo necesitas un VPS con Linux.

Para hosting, yo uso [Hetzner VPS desde 4 €/mes](https://go.bitdoze.com/hetzner) para la mayoría de mis proyectos — fiable, barato y con sede en la UE. [Hostinger VPS desde 5,99 $/mes](https://go.bitdoze.com/hostinger-vps) es otra opción económica si prefieres un proveedor diferente.

_Los precios de los VPS subieron en todos los proveedores en 2026 — si estás replanteándote una máquina alquilada, consulta [qué cambió y cuándo gana un mini PC](/es/subidas-precios-vps/)._

Para contenerizar tu app, consulta nuestra guía sobre [cómo ejecutar cualquier app en Python con Docker](/docker-run-python/). Si prefieres un flujo de trabajo PaaS, mira [desplegar un proyecto en Python con Dokploy](/es/dokploy-python-railpack-uv/) para una configuración de despliegue autoalojada.

## Conclusión

Has pasado de cero a una app de FastHTML funcional con interactividad, formularios y una lista de tareas — todo en Python puro. Esto es lo que cubrimos:

- Instalar FastHTML con pip o uv
- La sintaxis de función a HTML que hace funcionar a FastHTML
- Construir páginas con `fast_app()` y `@rt()`
- Crear componentes de UI: formularios, tablas, navegación
- Añadir interactividad con HTMX sin JavaScript
- Construir una app de tareas completa con añadir, marcar y eliminar
- Solucionar errores comunes
- Ejecutar en producción detrás de un proxy inverso

Desde aquí, los siguientes pasos naturales son:

- [Construye un sitio web multipágina con FastHTML](/es/fasthtml-multiples-paginas/) — enrutamiento, layouts y navegación entre páginas
- [Añade una base de datos SQLite a FastHTML](/es/fasthtml-sqlite-db/) — almacenamiento persistente con Fastlite
- [Añade autenticación](/es/fasthtml-autenticacion-usuarios/) — OAuth, sesiones y control de acceso

Y si quieres ver cómo se compara FastHTML con otros frameworks, [compara los frameworks web de Python](/best-python-web-frameworks/) para una visión más amplia.