← Volver al listado de tecnologías
Proyecto Final: API REST con Stdlib
En este capitulo final construiremos una API REST completa de gestion de tareas usando exclusivamente la biblioteca estandar de Go. Sin frameworks, sin dependencias externas.
Estructura del Proyecto
tasks-api/
├── main.go
├── model.go
├── store.go
├── handlers.go
├── middleware.go
└── main_test.go
Modelo
Definimos el modelo Task con tags JSON para la serializacion:
// model.go
package main
import "time"
type Task struct {
ID string `json:"id"`
Titulo string `json:"titulo"`
Descripcion string `json:"descripcion,omitempty"`
Completada bool `json:"completada"`
CreadaEn time.Time `json:"creada_en"`
}
Almacenamiento en Memoria
Usamos sync.RWMutex para acceso concurrente seguro:
// store.go
package main
import (
"fmt"
"sync"
"time"
)
type TaskStore struct {
mu sync.RWMutex
tasks map[string]Task
nextID int
}
func NewTaskStore() *TaskStore {
return &TaskStore{
tasks: make(map[string]Task),
}
}
func (s *TaskStore) All() []Task {
s.mu.RLock()
defer s.mu.RUnlock()
result := make([]Task, 0, len(s.tasks))
for _, t := range s.tasks {
result = append(result, t)
}
return result
}
func (s *TaskStore) Get(id string) (Task, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
t, ok := s.tasks[id]
return t, ok
}
func (s *TaskStore) Create(t Task) Task {
s.mu.Lock()
defer s.mu.Unlock()
s.nextID++
t.ID = fmt.Sprintf("%d", s.nextID)
t.CreadaEn = time.Now()
s.tasks[t.ID] = t
return t
}
func (s *TaskStore) Update(id string, t Task) (Task, bool) {
s.mu.Lock()
defer s.mu.Unlock()
existing, ok := s.tasks[id]
if !ok {
return Task{}, false
}
t.ID = id
t.CreadaEn = existing.CreadaEn
s.tasks[id] = t
return t, true
}
func (s *TaskStore) Delete(id string) bool {
s.mu.Lock()
defer s.mu.Unlock()
_, ok := s.tasks[id]
if ok {
delete(s.tasks, id)
}
return ok
}
| Metodo | Lock | Descripcion |
|---|---|---|
All() | RLock | Lectura multiple concurrente |
Get() | RLock | Lectura por ID |
Create() | Lock | Escritura exclusiva |
Update() | Lock | Escritura exclusiva |
Delete() | Lock | Escritura exclusiva |
Handlers CRUD
Cada handler maneja una operacion especifica:
// handlers.go
package main
import (
"encoding/json"
"net/http"
)
type TaskHandler struct {
store *TaskStore
}
func NewTaskHandler(store *TaskStore) *TaskHandler {
return &TaskHandler{store: store}
}
func (h *TaskHandler) List(w http.ResponseWriter, r *http.Request) {
tasks := h.store.All()
writeJSON(w, http.StatusOK, tasks)
}
func (h *TaskHandler) GetByID(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
task, ok := h.store.Get(id)
if !ok {
writeJSON(w, http.StatusNotFound, map[string]string{
"error": "tarea no encontrada",
})
return
}
writeJSON(w, http.StatusOK, task)
}
func (h *TaskHandler) Create(w http.ResponseWriter, r *http.Request) {
var task Task
if err := json.NewDecoder(r.Body).Decode(&task); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{
"error": "JSON invalido",
})
return
}
if task.Titulo == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{
"error": "titulo es requerido",
})
return
}
created := h.store.Create(task)
writeJSON(w, http.StatusCreated, created)
}
func (h *TaskHandler) Update(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
var task Task
if err := json.NewDecoder(r.Body).Decode(&task); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{
"error": "JSON invalido",
})
return
}
updated, ok := h.store.Update(id, task)
if !ok {
writeJSON(w, http.StatusNotFound, map[string]string{
"error": "tarea no encontrada",
})
return
}
writeJSON(w, http.StatusOK, updated)
}
func (h *TaskHandler) Delete(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if !h.store.Delete(id) {
writeJSON(w, http.StatusNotFound, map[string]string{
"error": "tarea no encontrada",
})
return
}
w.WriteHeader(http.StatusNoContent)
}
func writeJSON(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
Middleware
Dos middlewares: logging y content-type:
// middleware.go
package main
import (
"log"
"net/http"
"time"
)
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start))
})
}
func JSONContentType(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
next.ServeHTTP(w, r)
})
}
Router y Main
Usamos http.ServeMux de Go 1.22+ con patrones de metodo:
// main.go
package main
import (
"fmt"
"log"
"net/http"
"os"
)
func main() {
store := NewTaskStore()
handler := NewTaskHandler(store)
mux := http.NewServeMux()
// Go 1.22: METHOD /pattern
mux.HandleFunc("GET /tasks", handler.List)
mux.HandleFunc("GET /tasks/{id}", handler.GetByID)
mux.HandleFunc("POST /tasks", handler.Create)
mux.HandleFunc("PUT /tasks/{id}", handler.Update)
mux.HandleFunc("DELETE /tasks/{id}", handler.Delete)
// Health check
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
})
// Aplicar middlewares
wrapped := LoggingMiddleware(JSONContentType(mux))
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
addr := fmt.Sprintf(":%s", port)
log.Printf("Servidor iniciado en %s", addr)
log.Fatal(http.ListenAndServe(addr, wrapped))
}
Tabla de Endpoints
| Metodo | Ruta | Descripcion | Status |
|---|---|---|---|
| GET | /tasks | Listar todas | 200 |
| GET | /tasks/{id} | Obtener por ID | 200 / 404 |
| POST | /tasks | Crear tarea | 201 / 400 |
| PUT | /tasks/{id} | Actualizar tarea | 200 / 404 |
| DELETE | /tasks/{id} | Eliminar tarea | 204 / 404 |
| GET | /health | Health check | 200 |
Tests con httptest
// main_test.go
package main
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func setupRouter() http.Handler {
store := NewTaskStore()
handler := NewTaskHandler(store)
mux := http.NewServeMux()
mux.HandleFunc("GET /tasks", handler.List)
mux.HandleFunc("GET /tasks/{id}", handler.GetByID)
mux.HandleFunc("POST /tasks", handler.Create)
mux.HandleFunc("PUT /tasks/{id}", handler.Update)
mux.HandleFunc("DELETE /tasks/{id}", handler.Delete)
return mux
}
func TestCreateTask(t *testing.T) {
router := setupRouter()
body := `{"titulo":"Aprender Go","descripcion":"Completar tutorial"}`
req := httptest.NewRequest("POST", "/tasks", bytes.NewBufferString(body))
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusCreated {
t.Fatalf("esperado %d, obtenido %d", http.StatusCreated, w.Code)
}
var task Task
json.NewDecoder(w.Body).Decode(&task)
if task.Titulo != "Aprender Go" {
t.Fatalf("titulo esperado 'Aprender Go', obtenido '%s'", task.Titulo)
}
if task.ID == "" {
t.Fatal("ID no deberia estar vacio")
}
}
func TestListTasks(t *testing.T) {
router := setupRouter()
// Crear una tarea primero
body := `{"titulo":"Tarea 1"}`
req := httptest.NewRequest("POST", "/tasks", bytes.NewBufferString(body))
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// Listar tareas
req = httptest.NewRequest("GET", "/tasks", nil)
w = httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("esperado %d, obtenido %d", http.StatusOK, w.Code)
}
var tasks []Task
json.NewDecoder(w.Body).Decode(&tasks)
if len(tasks) != 1 {
t.Fatalf("esperada 1 tarea, obtenidas %d", len(tasks))
}
}
func TestGetTaskNotFound(t *testing.T) {
router := setupRouter()
req := httptest.NewRequest("GET", "/tasks/999", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusNotFound {
t.Fatalf("esperado %d, obtenido %d", http.StatusNotFound, w.Code)
}
}
func TestCreateTaskValidation(t *testing.T) {
router := setupRouter()
body := `{"descripcion":"sin titulo"}`
req := httptest.NewRequest("POST", "/tasks", bytes.NewBufferString(body))
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("esperado %d, obtenido %d", http.StatusBadRequest, w.Code)
}
}
Ejecutar los tests:
go test -v ./...
Ejemplo de Uso con curl
Iniciar el servidor:
go run .
# Servidor iniciado en :8080
# Crear tarea
curl -X POST http://localhost:8080/tasks \
-H "Content-Type: application/json" \
-d '{"titulo":"Aprender Go","descripcion":"Completar el tutorial"}'
# Listar tareas
curl http://localhost:8080/tasks
# Obtener por ID
curl http://localhost:8080/tasks/1
# Actualizar
curl -X PUT http://localhost:8080/tasks/1 \
-H "Content-Type: application/json" \
-d '{"titulo":"Aprender Go","completada":true}'
# Eliminar
curl -X DELETE http://localhost:8080/tasks/1
Resumen del Proyecto
| Componente | Archivo | Responsabilidad |
|---|---|---|
| Modelo | model.go | Estructura Task con JSON tags |
| Store | store.go | CRUD en memoria con RWMutex |
| Handlers | handlers.go | Logica HTTP, validacion, respuestas |
| Middleware | middleware.go | Logging y Content-Type |
| Router | main.go | Rutas, configuracion, arranque |
| Tests | main_test.go | Tests con httptest |
| Concepto Go Aplicado | Donde |
|---|---|
| Structs y metodos | Store, Handler |
| Interfaces (http.Handler) | Middleware |
| Goroutine safety (sync.RWMutex) | Store |
| JSON encoding/decoding | Handlers |
| Testing con httptest | Tests |
| ServeMux Go 1.22 con patrones | Router |
| Variables de entorno | Configuracion de puerto |