Cristhian Villegas
Backend12 min read2 views

Curso Go #10: Proyecto Final - API REST con Go

Curso Go #10: Proyecto Final - API REST con Go

Proyecto Final: API REST con Go - Parte 10 de 10

Logo de Go

Bienvenido al articulo final (10 de 10) del Curso de Go desde Cero. A lo largo de 9 articulos anteriores aprendimos los fundamentos del lenguaje: variables, funciones, structs, interfaces, concurrencia, manejo de errores y testing. Ahora es el momento de aplicar todo en un proyecto real.

Vamos a construir una API REST completa para gestionar tareas (todo list). Usaremos exclusivamente la biblioteca estandar de Go (sin frameworks externos), lo que te dara una comprension profunda de como funcionan las APIs en Go por dentro.

Nota: Este proyecto usa Go 1.22+ que incluye el nuevo ServeMux con soporte para metodos HTTP y patrones mejorados. Si usas una version anterior, necesitaras adaptar las rutas.

Configuracion del proyecto

Primero, vamos a crear la estructura del proyecto siguiendo las convenciones de Go:

bash
1# Crear el directorio del proyecto
2mkdir task-api
3cd task-api
4
5# Inicializar el modulo de Go
6go mod init github.com/tu-usuario/task-api
7
8# Crear la estructura de carpetas
9mkdir -p cmd/server
10mkdir -p internal/task
11mkdir -p internal/middleware

La estructura final del proyecto sera:

bash
1task-api/
2  cmd/
3    server/
4      main.go          # Punto de entrada de la aplicacion
5  internal/
6    task/
7      model.go         # Definicion del struct Task
8      store.go         # Almacenamiento en memoria
9      handler.go       # Handlers HTTP para el CRUD
10    middleware/
11      logging.go       # Middleware de logging
12      cors.go          # Middleware de CORS
13  go.mod
Tip: La carpeta cmd/ contiene los puntos de entrada de la aplicacion. La carpeta internal/ contiene codigo que solo puede ser importado por este modulo (Go impide que otros modulos importen paquetes de internal/). Esta es una convencion estandar en proyectos Go.

Definiendo el modelo Task

Empezamos definiendo la estructura de datos que representa una tarea. Crea el archivo internal/task/model.go:

go
1// internal/task/model.go
2package task
3
4import "time"
5
6// Status representa el estado de una tarea.
7type Status string
8
9const (
10    StatusPending   Status = "pending"
11    StatusCompleted Status = "completed"
12)
13
14// Task representa una tarea en el sistema.
15type Task struct {
16    ID          int       "json:\"id\""
17    Title       string    "json:\"title\""
18    Description string    "json:\"description\""
19    Status      Status    "json:\"status\""
20    CreatedAt   time.Time "json:\"created_at\""
21    UpdatedAt   time.Time "json:\"updated_at\""
22}
23
24// CreateTaskRequest es el payload para crear una tarea.
25type CreateTaskRequest struct {
26    Title       string "json:\"title\""
27    Description string "json:\"description\""
28}
29
30// UpdateTaskRequest es el payload para actualizar una tarea.
31type UpdateTaskRequest struct {
32    Title       *string "json:\"title\""
33    Description *string "json:\"description\""
34    Status      *Status "json:\"status\""
35}
36
37// Validate verifica que el request de creacion sea valido.
38func (r CreateTaskRequest) Validate() []string {
39    var errors []string
40    if r.Title == "" {
41        errors = append(errors, "title is required")
42    }
43    if len(r.Title) > 200 {
44        errors = append(errors, "title must be 200 characters or less")
45    }
46    return errors
47}
48
49// Validate verifica que el request de actualizacion sea valido.
50func (r UpdateTaskRequest) Validate() []string {
51    var errors []string
52    if r.Title != nil && *r.Title == "" {
53        errors = append(errors, "title cannot be empty")
54    }
55    if r.Title != nil && len(*r.Title) > 200 {
56        errors = append(errors, "title must be 200 characters or less")
57    }
58    if r.Status != nil && *r.Status != StatusPending && *r.Status != StatusCompleted {
59        errors = append(errors, "status must be pending or completed")
60    }
61    return errors
62}
Nota: Usamos punteros (*string, *Status) en UpdateTaskRequest para distinguir entre "el campo no fue enviado" (nil) y "el campo fue enviado vacio". Esta es una tecnica comun en APIs REST para soportar actualizaciones parciales (PATCH).

Almacenamiento en memoria con sync.Mutex

Para mantener el proyecto simple, usaremos almacenamiento en memoria con proteccion para concurrencia. Crea internal/task/store.go:

go
1// internal/task/store.go
2package task
3
4import (
5    "errors"
6    "sync"
7    "time"
8)
9
10var (
11    ErrNotFound = errors.New("task not found")
12)
13
14// Store gestiona el almacenamiento de tareas en memoria.
15type Store struct {
16    mu     sync.RWMutex
17    tasks  map[int]Task
18    nextID int
19}
20
21// NewStore crea un nuevo almacen de tareas.
22func NewStore() *Store {
23    return &Store{
24        tasks:  make(map[int]Task),
25        nextID: 1,
26    }
27}
28
29// GetAll retorna todas las tareas.
30func (s *Store) GetAll() []Task {
31    s.mu.RLock()
32    defer s.mu.RUnlock()
33
34    result := make([]Task, 0, len(s.tasks))
35    for _, t := range s.tasks {
36        result = append(result, t)
37    }
38    return result
39}
40
41// GetByID retorna una tarea por su ID.
42func (s *Store) GetByID(id int) (Task, error) {
43    s.mu.RLock()
44    defer s.mu.RUnlock()
45
46    t, ok := s.tasks[id]
47    if !ok {
48        return Task{}, ErrNotFound
49    }
50    return t, nil
51}
52
53// Create crea una nueva tarea y retorna la tarea creada.
54func (s *Store) Create(req CreateTaskRequest) Task {
55    s.mu.Lock()
56    defer s.mu.Unlock()
57
58    now := time.Now()
59    t := Task{
60        ID:          s.nextID,
61        Title:       req.Title,
62        Description: req.Description,
63        Status:      StatusPending,
64        CreatedAt:   now,
65        UpdatedAt:   now,
66    }
67    s.tasks[s.nextID] = t
68    s.nextID++
69    return t
70}
71
72// Update actualiza una tarea existente.
73func (s *Store) Update(id int, req UpdateTaskRequest) (Task, error) {
74    s.mu.Lock()
75    defer s.mu.Unlock()
76
77    t, ok := s.tasks[id]
78    if !ok {
79        return Task{}, ErrNotFound
80    }
81
82    if req.Title != nil {
83        t.Title = *req.Title
84    }
85    if req.Description != nil {
86        t.Description = *req.Description
87    }
88    if req.Status != nil {
89        t.Status = *req.Status
90    }
91    t.UpdatedAt = time.Now()
92
93    s.tasks[id] = t
94    return t, nil
95}
96
97// Delete elimina una tarea por su ID.
98func (s *Store) Delete(id int) error {
99    s.mu.Lock()
100    defer s.mu.Unlock()
101
102    if _, ok := s.tasks[id]; !ok {
103        return ErrNotFound
104    }
105    delete(s.tasks, id)
106    return nil
107}
Advertencia: Usamos sync.RWMutex en lugar de sync.Mutex regular. RWMutex permite multiples lecturas simultaneas (RLock) pero solo una escritura a la vez (Lock). Esto mejora el rendimiento cuando hay muchas mas lecturas que escrituras, que es el caso tipico en una API.

Handlers HTTP para el CRUD

Ahora implementamos los handlers que procesan las peticiones HTTP. Crea internal/task/handler.go:

go
1// internal/task/handler.go
2package task
3
4import (
5    "encoding/json"
6    "errors"
7    "net/http"
8    "strconv"
9)
10
11// Handler contiene los handlers HTTP para tareas.
12type Handler struct {
13    store *Store
14}
15
16// NewHandler crea un nuevo handler con el store proporcionado.
17func NewHandler(store *Store) *Handler {
18    return &Handler{store: store}
19}
20
21// respuesta JSON generica
22type errorResponse struct {
23    Error   string   "json:\"error\""
24    Details []string "json:\"details,omitempty\""
25}
26
27func writeJSON(w http.ResponseWriter, status int, data interface{}) {
28    w.Header().Set("Content-Type", "application/json")
29    w.WriteHeader(status)
30    json.NewEncoder(w).Encode(data)
31}
32
33func writeError(w http.ResponseWriter, status int, msg string, details []string) {
34    writeJSON(w, status, errorResponse{Error: msg, Details: details})
35}
36
37// HandleGetAll maneja GET /tasks
38func (h *Handler) HandleGetAll(w http.ResponseWriter, r *http.Request) {
39    tasks := h.store.GetAll()
40    writeJSON(w, http.StatusOK, tasks)
41}
42
43// HandleGetByID maneja GET /tasks/{id}
44func (h *Handler) HandleGetByID(w http.ResponseWriter, r *http.Request) {
45    id, err := strconv.Atoi(r.PathValue("id"))
46    if err != nil {
47        writeError(w, http.StatusBadRequest, "invalid task ID", nil)
48        return
49    }
50
51    task, err := h.store.GetByID(id)
52    if err != nil {
53        if errors.Is(err, ErrNotFound) {
54            writeError(w, http.StatusNotFound, "task not found", nil)
55            return
56        }
57        writeError(w, http.StatusInternalServerError, "internal error", nil)
58        return
59    }
60
61    writeJSON(w, http.StatusOK, task)
62}
63
64// HandleCreate maneja POST /tasks
65func (h *Handler) HandleCreate(w http.ResponseWriter, r *http.Request) {
66    var req CreateTaskRequest
67    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
68        writeError(w, http.StatusBadRequest, "invalid JSON body", nil)
69        return
70    }
71
72    if errs := req.Validate(); len(errs) > 0 {
73        writeError(w, http.StatusBadRequest, "validation failed", errs)
74        return
75    }
76
77    task := h.store.Create(req)
78    writeJSON(w, http.StatusCreated, task)
79}
80
81// HandleUpdate maneja PUT /tasks/{id}
82func (h *Handler) HandleUpdate(w http.ResponseWriter, r *http.Request) {
83    id, err := strconv.Atoi(r.PathValue("id"))
84    if err != nil {
85        writeError(w, http.StatusBadRequest, "invalid task ID", nil)
86        return
87    }
88
89    var req UpdateTaskRequest
90    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
91        writeError(w, http.StatusBadRequest, "invalid JSON body", nil)
92        return
93    }
94
95    if errs := req.Validate(); len(errs) > 0 {
96        writeError(w, http.StatusBadRequest, "validation failed", errs)
97        return
98    }
99
100    task, err := h.store.Update(id, req)
101    if err != nil {
102        if errors.Is(err, ErrNotFound) {
103            writeError(w, http.StatusNotFound, "task not found", nil)
104            return
105        }
106        writeError(w, http.StatusInternalServerError, "internal error", nil)
107        return
108    }
109
110    writeJSON(w, http.StatusOK, task)
111}
112
113// HandleDelete maneja DELETE /tasks/{id}
114func (h *Handler) HandleDelete(w http.ResponseWriter, r *http.Request) {
115    id, err := strconv.Atoi(r.PathValue("id"))
116    if err != nil {
117        writeError(w, http.StatusBadRequest, "invalid task ID", nil)
118        return
119    }
120
121    if err := h.store.Delete(id); err != nil {
122        if errors.Is(err, ErrNotFound) {
123            writeError(w, http.StatusNotFound, "task not found", nil)
124            return
125        }
126        writeError(w, http.StatusInternalServerError, "internal error", nil)
127        return
128    }
129
130    w.WriteHeader(http.StatusNoContent)
131}
Tip: r.PathValue("id") es una funcion nueva de Go 1.22+ que extrae valores de los parametros de ruta. Antes de Go 1.22, tendrias que parsear la URL manualmente o usar un router externo como gorilla/mux.

Middleware: Logging y CORS

Los middlewares son funciones que envuelven a los handlers para agregar funcionalidad transversal. Vamos a crear dos middlewares esenciales.

Crea internal/middleware/logging.go:

go
1// internal/middleware/logging.go
2package middleware
3
4import (
5    "log"
6    "net/http"
7    "time"
8)
9
10// statusRecorder captura el status code de la respuesta.
11type statusRecorder struct {
12    http.ResponseWriter
13    statusCode int
14}
15
16func (r *statusRecorder) WriteHeader(code int) {
17    r.statusCode = code
18    r.ResponseWriter.WriteHeader(code)
19}
20
21// Logging registra cada peticion HTTP con metodo, ruta, status y duracion.
22func Logging(next http.Handler) http.Handler {
23    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
24        start := time.Now()
25
26        recorder := &statusRecorder{
27            ResponseWriter: w,
28            statusCode:     http.StatusOK,
29        }
30
31        next.ServeHTTP(recorder, r)
32
33        log.Printf("%s %s %d %s",
34            r.Method,
35            r.URL.Path,
36            recorder.statusCode,
37            time.Since(start),
38        )
39    })
40}

Crea internal/middleware/cors.go:

go
1// internal/middleware/cors.go
2package middleware
3
4import "net/http"
5
6// CORS agrega los headers necesarios para Cross-Origin Resource Sharing.
7func CORS(next http.Handler) http.Handler {
8    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
9        w.Header().Set("Access-Control-Allow-Origin", "*")
10        w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
11        w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
12
13        // Responder inmediatamente a las peticiones preflight
14        if r.Method == http.MethodOptions {
15            w.WriteHeader(http.StatusOK)
16            return
17        }
18
19        next.ServeHTTP(w, r)
20    })
21}

Punto de entrada: main.go

Finalmente, conectamos todo en el archivo principal. Crea cmd/server/main.go:

go
1// cmd/server/main.go
2package main
3
4import (
5    "fmt"
6    "log"
7    "net/http"
8    "os"
9
10    "github.com/tu-usuario/task-api/internal/middleware"
11    "github.com/tu-usuario/task-api/internal/task"
12)
13
14func main() {
15    // Configuracion
16    port := os.Getenv("PORT")
17    if port == "" {
18        port = "8080"
19    }
20
21    // Crear el store y el handler
22    store := task.NewStore()
23    handler := task.NewHandler(store)
24
25    // Configurar las rutas con el nuevo ServeMux de Go 1.22+
26    mux := http.NewServeMux()
27
28    mux.HandleFunc("GET /tasks", handler.HandleGetAll)
29    mux.HandleFunc("GET /tasks/{id}", handler.HandleGetByID)
30    mux.HandleFunc("POST /tasks", handler.HandleCreate)
31    mux.HandleFunc("PUT /tasks/{id}", handler.HandleUpdate)
32    mux.HandleFunc("DELETE /tasks/{id}", handler.HandleDelete)
33
34    // Health check
35    mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
36        w.Header().Set("Content-Type", "application/json")
37        w.Write([]byte("{"status":"ok"}"))
38    })
39
40    // Aplicar middlewares
41    var h http.Handler = mux
42    h = middleware.Logging(h)
43    h = middleware.CORS(h)
44
45    // Iniciar el servidor
46    addr := fmt.Sprintf(":%s", port)
47    log.Printf("Servidor iniciado en http://localhost%s", addr)
48    log.Printf("Endpoints disponibles:")
49    log.Printf("  GET    /tasks       - Listar todas las tareas")
50    log.Printf("  GET    /tasks/{id}  - Obtener una tarea por ID")
51    log.Printf("  POST   /tasks       - Crear una nueva tarea")
52    log.Printf("  PUT    /tasks/{id}  - Actualizar una tarea")
53    log.Printf("  DELETE /tasks/{id}  - Eliminar una tarea")
54    log.Printf("  GET    /health      - Health check")
55
56    if err := http.ListenAndServe(addr, h); err != nil {
57        log.Fatalf("Error al iniciar el servidor: %v", err)
58    }
59}

Probando la API con curl

Una vez que el servidor esta corriendo, puedes probarlo con curl. Primero, inicia el servidor:

bash
1# Iniciar el servidor
2go run cmd/server/main.go

En otra terminal, prueba cada endpoint:

bash
1# 1. Crear una tarea
2curl -X POST http://localhost:8080/tasks   -H "Content-Type: application/json"   -d '{"title":"Aprender Go","description":"Completar el curso de Go desde cero"}'
3
4# Respuesta:
5# {"id":1,"title":"Aprender Go","description":"Completar el curso de Go desde cero",
6#  "status":"pending","created_at":"2026-04-05T10:00:00Z","updated_at":"2026-04-05T10:00:00Z"}
7
8# 2. Crear otra tarea
9curl -X POST http://localhost:8080/tasks   -H "Content-Type: application/json"   -d '{"title":"Construir una API","description":"Aplicar lo aprendido en un proyecto real"}'
10
11# 3. Listar todas las tareas
12curl http://localhost:8080/tasks
13
14# 4. Obtener una tarea por ID
15curl http://localhost:8080/tasks/1
16
17# 5. Actualizar una tarea
18curl -X PUT http://localhost:8080/tasks/1   -H "Content-Type: application/json"   -d '{"status":"completed"}'
19
20# 6. Eliminar una tarea
21curl -X DELETE http://localhost:8080/tasks/2
22
23# 7. Verificar que se elimino
24curl http://localhost:8080/tasks
25
26# 8. Health check
27curl http://localhost:8080/health
Tip: Si no tienes curl instalado, puedes usar herramientas como Postman, Insomnia o la extension REST Client de VS Code para probar tu API.

Manejo de errores y respuestas JSON

Observa como nuestro proyecto maneja los errores de forma consistente. Cada error retorna un JSON con un formato estandar:

go
1// Respuesta de error estandar
2// {"error":"descripcion del error","details":["detalle 1","detalle 2"]}
3
4// Ejemplo: intentar obtener una tarea que no existe
5// GET /tasks/999
6// Status: 404
7// {"error":"task not found"}
8
9// Ejemplo: enviar JSON invalido
10// POST /tasks con body "esto no es json"
11// Status: 400
12// {"error":"invalid JSON body"}
13
14// Ejemplo: validacion fallida
15// POST /tasks con body {"title":""}
16// Status: 400
17// {"error":"validation failed","details":["title is required"]}

Este patron de respuestas consistentes es fundamental en APIs profesionales. Los clientes de tu API siempre saben que formato esperar, tanto para respuestas exitosas como para errores.

Nota: En una API de produccion, tambien querrias agregar un campo code numerico o un identificador unico de error para que los clientes puedan manejar errores especificos programaticamente, sin depender del texto del mensaje.

Proximos pasos

Este proyecto es un excelente punto de partida, pero hay mucho mas que puedes agregar para llevarlo a nivel de produccion:

Base de datos

Reemplazar el almacenamiento en memoria por una base de datos real como PostgreSQL usando el paquete database/sql o un ORM como GORM. Esto permite que los datos persistan entre reinicios del servidor.

Frameworks populares

Aunque la biblioteca estandar de Go es muy potente, frameworks como Gin, Echo o Fiber ofrecen caracteristicas adicionales como binding automatico de parametros, validacion integrada, grupos de rutas y middleware pre-construido.

Autenticacion

Agregar autenticacion con JWT (JSON Web Tokens) para proteger los endpoints. Cada usuario solo deberia poder ver y modificar sus propias tareas.

Docker y despliegue

Crear un Dockerfile para containerizar tu API y desplegarla en servicios como AWS ECS, Google Cloud Run o un VPS con Docker Compose.

Documentacion con Swagger

Usar herramientas como swag para generar documentacion interactiva de tu API automaticamente a partir de comentarios en el codigo.

Conclusion del curso

¡Felicidades! Has completado el Curso de Go desde Cero en 10 articulos. Hagamos un repaso rapido de todo lo que aprendiste:

  • Articulo 1: Instalacion y configuracion del entorno de Go
  • Articulo 2: Variables, tipos de datos y operadores
  • Articulo 3: Estructuras de control: if, switch, for
  • Articulo 4: Funciones, parametros y valores de retorno multiples
  • Articulo 5: Structs, metodos y composicion
  • Articulo 6: Interfaces y polimorfismo
  • Articulo 7: Concurrencia: goroutines y channels
  • Articulo 8: Manejo de errores y paquetes
  • Articulo 9: Testing y benchmarking
  • Articulo 10: Proyecto final — API REST completa

Ahora tienes las bases solidas para seguir aprendiendo Go y construir aplicaciones reales. Go es un lenguaje que brilla en servidores web, microservicios, herramientas de linea de comandos y sistemas distribuidos. Las empresas mas grandes del mundo (Google, Uber, Netflix, Docker, Kubernetes) usan Go en produccion.

Tip: La mejor forma de seguir aprendiendo es construir proyectos. Algunas ideas: un acortador de URLs, un chat en tiempo real con WebSockets, un CLI para automatizar tareas, o un scraper web. Cada proyecto te ensenara algo nuevo.

¡Gracias por seguir este curso y mucho exito en tu camino como desarrollador Go!

Share:
CV

Cristhian Villegas

Software Engineer specializing in Java, Spring Boot, Angular & AWS. Building scalable distributed systems with clean architecture.

Comments

Sign in to leave a comment

No comments yet. Be the first!

Related Articles