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

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.
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:
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:
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
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:
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}
*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:
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}
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:
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}
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:
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:
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:
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:
1# Iniciar el servidor
2go run cmd/server/main.go
En otra terminal, prueba cada endpoint:
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
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:
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.
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.
¡Gracias por seguir este curso y mucho exito en tu camino como desarrollador Go!
Comments
Sign in to leave a comment
No comments yet. Be the first!