Proyecto Todo List: Estructura y Modelo de Datos

Por: Artiko
gotemplhtmxproyectotodo-list

Hora de poner en practica todo lo aprendido. Vamos a construir una Todo List completa con Go + Templ + HTMX. En este capitulo definimos la arquitectura, los modelos de datos y la base del proyecto.

Arquitectura del Proyecto

Organizamos el codigo en packages con responsabilidades claras:

todo-app/
├── main.go              # Punto de entrada, rutas y servidor
├── go.mod
├── go.sum
├── models/
   └── todo.go          # Struct Todo y Store
├── handlers/
   └── todo.go          # Handlers HTTP
├── views/
   ├── layout.templ     # Layout base
   ├── todo_list.templ  # Lista de todos
   ├── todo_item.templ  # Item individual
   └── todo_form.templ  # Formulario de creacion
└── static/
    └── styles.css       # Estilos CSS
PackageResponsabilidad
modelsEstructuras de datos y logica de almacenamiento
handlersRecibir HTTP requests y devolver respuestas
viewsTemplates Templ (la UI)
staticArchivos estaticos (CSS)

Esta separacion sigue el principio de responsabilidad unica: cada package hace una sola cosa.

Inicializar el Proyecto

mkdir todo-app && cd todo-app
go mod init todo-app
go get github.com/a-h/templ

Modelo de Datos

El struct Todo representa una tarea. Lo definimos en models/todo.go:

package models

import "time"

type Todo struct {
    ID        int       `json:"id"`
    Title     string    `json:"title"`
    Completed bool      `json:"completed"`
    CreatedAt time.Time `json:"created_at"`
}

Cuatro campos son suficientes. ID identifica cada tarea, Completed controla su estado y CreatedAt registra cuando se creo.

Store en Memoria

Para este proyecto usamos un slice en memoria protegido con sync.RWMutex para manejar concurrencia. En una app real usarias una base de datos.

package models

import (
    "sync"
    "sync/atomic"
    "time"
)

type TodoStore struct {
    mu     sync.RWMutex
    todos  []Todo
    nextID atomic.Int64
}

func NewTodoStore() *TodoStore {
    return &TodoStore{}
}

El atomic.Int64 genera IDs unicos sin necesidad de un mutex adicional. Cada vez que agregamos un todo, incrementamos el contador.

Metodos del Store

Obtener Todos

func (s *TodoStore) GetAll() []Todo {
    s.mu.RLock()
    defer s.mu.RUnlock()

    result := make([]Todo, len(s.todos))
    copy(result, s.todos)
    return result
}

Usamos RLock (read lock) porque solo leemos. Retornamos una copia para evitar data races.

Obtener por ID

func (s *TodoStore) GetByID(id int) (Todo, bool) {
    s.mu.RLock()
    defer s.mu.RUnlock()

    for _, t := range s.todos {
        if t.ID == id {
            return t, true
        }
    }
    return Todo{}, false
}

Agregar

func (s *TodoStore) Add(title string) Todo {
    s.mu.Lock()
    defer s.mu.Unlock()

    todo := Todo{
        ID:        int(s.nextID.Add(1)),
        Title:     title,
        Completed: false,
        CreatedAt: time.Now(),
    }
    s.todos = append(s.todos, todo)
    return todo
}

nextID.Add(1) incrementa atomicamente y devuelve el nuevo valor.

Toggle Completado

func (s *TodoStore) Toggle(id int) (Todo, bool) {
    s.mu.Lock()
    defer s.mu.Unlock()

    for i, t := range s.todos {
        if t.ID == id {
            s.todos[i].Completed = !t.Completed
            return s.todos[i], true
        }
    }
    return Todo{}, false
}

Actualizar Titulo

func (s *TodoStore) Update(id int, title string) (Todo, bool) {
    s.mu.Lock()
    defer s.mu.Unlock()

    for i, t := range s.todos {
        if t.ID == id {
            s.todos[i].Title = title
            return s.todos[i], true
        }
    }
    return Todo{}, false
}

Eliminar

func (s *TodoStore) Delete(id int) bool {
    s.mu.Lock()
    defer s.mu.Unlock()

    for i, t := range s.todos {
        if t.ID == id {
            s.todos = append(s.todos[:i], s.todos[i+1:]...)
            return true
        }
    }
    return false
}

main.go: Rutas y Servidor

El punto de entrada conecta todo: crea el store, registra las rutas y arranca el servidor.

package main

import (
    "fmt"
    "log"
    "net/http"
    "todo-app/handlers"
    "todo-app/models"
)

func main() {
    store := models.NewTodoStore()
    h := handlers.NewTodoHandler(store)

    mux := http.NewServeMux()

    // Archivos estaticos
    mux.Handle("GET /static/",
        http.StripPrefix("/static/",
            http.FileServer(http.Dir("static"))))

    // Rutas de la app
    mux.HandleFunc("GET /", h.Index)
    mux.HandleFunc("GET /todos", h.List)
    mux.HandleFunc("POST /todos", h.Create)
    mux.HandleFunc("PUT /todos/{id}/toggle", h.Toggle)
    mux.HandleFunc("PUT /todos/{id}", h.Update)
    mux.HandleFunc("DELETE /todos/{id}", h.Delete)

    fmt.Println("Servidor en http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}

Usamos el nuevo router de Go 1.22+ que soporta metodos HTTP y path parameters como {id}.

Layout Base en Templ

El layout envuelve todas las paginas con el HTML base. Creamos views/layout.templ:

package views

templ Layout(title string) {
    <!DOCTYPE html>
    <html lang="es">
    <head>
        <meta charset="UTF-8"/>
        <meta name="viewport"
              content="width=device-width, initial-scale=1.0"/>
        <title>{ title }</title>
        <link rel="stylesheet" href="/static/styles.css"/>
        <script src="https://unpkg.com/[email protected]"></script>
    </head>
    <body>
        <main class="container">
            <h1>Todo List</h1>
            { children... }
        </main>
    </body>
    </html>
}

Puntos clave:

Handler Base

Creamos la estructura del handler en handlers/todo.go:

package handlers

import "todo-app/models"

type TodoHandler struct {
    store *models.TodoStore
}

func NewTodoHandler(store *models.TodoStore) *TodoHandler {
    return &TodoHandler{store: store}
}

El handler recibe el store por inyeccion de dependencias. En los siguientes capitulos implementaremos cada metodo.

Generar Codigo Templ

Despues de crear archivos .templ, genera el codigo Go:

templ generate

Esto crea archivos _templ.go junto a cada .templ. Estos archivos generados son los que Go compila.

Resumen

ElementoArchivoFuncion
Modelomodels/todo.goStruct Todo y TodoStore
Handlerhandlers/todo.goLogica HTTP
Vistasviews/*.templUI con Templ
Entradamain.goRutas y servidor

La arquitectura es simple pero escalable. Cada pieza tiene una responsabilidad clara y es facil de testear de forma independiente.


<— Capitulo 8: HTMX Avanzado | Capitulo 10: Listar y Crear Tareas —>