flowchart LR
Req["POST /api/tasks<br/>{ title: 'x' }"] --> M{"Mux / Router<br/>¿existe ruta+verbo?"}
M -->|"no"| R404["404 Not Found"]
M -->|"sí"| H["handler createTask"]
H -->|"éxito"| R201["201 Created + tarea"]
H -->|"body malo"| R400["400 Bad Request"]
12 3. Necesitamos responder a URLs y verbos
Qué es exactamente un endpoint y qué promete
Vas a construir tu primer CRUD real: cinco endpoints (GET, POST, DELETE…) que listan, crean y borran tareas en memoria — y a diseñar la tabla verbo+ruta que es el contrato de la API.
12.1 El problema
Taskflow necesita su primera funcionalidad real: gestionar tareas. El frontend (que ya sabes escribir) va a hacer esto:
await fetch("/api/tasks"); // listar
await fetch("/api/tasks", { method: "POST", … }); // crear
await fetch("/api/tasks/42"); // ver una
await fetch("/api/tasks/42", { method: "DELETE" });// borrarCada una de esas llamadas es una promesa que tu servidor debe cumplir: qué ruta atiende, qué verbo significa qué, qué código de estado devuelve. Diseñar eso es diseñar la API — y es una conversación de equipo antes que de código.
return hace dos cosas a la vez: devuelve el resultado Y termina la función — lo que esté debajo nunca corre. return task = “aquí está el plato, salgo de la cocina”.
12.2 Cómo lo resuelve un equipo
Antes de escribir handlers, un equipo se sienta sobre una tabla así:
| Verbo + ruta | Significado | Éxito | Error esperado |
|---|---|---|---|
GET /tasks |
listar todas | 200 + array |
— |
POST /tasks |
crear | 201 + tarea creada |
400 body inválido |
GET /tasks/{id} |
una tarea | 200 + tarea |
404 no existe |
PATCH /tasks/{id} |
modificar parcial | 200 + tarea |
404, 400 |
DELETE /tasks/{id} |
borrar | 204 sin body |
404 |
Dos decisiones de diseño ya están tomadas en esa tabla — fíjate:
- Sustantivos plurales, no verbos en la URL:
/tasks, no/getTasksni/createTask. El verbo ya lo pone el método HTTP. - Los códigos de estado son parte del contrato:
201dice “se creó”,204dice “salió bien y no hay nada que devolver”,404dice “eso no existe”. Devolver200con{"error": "not found"}es mentirle al cliente.
12.3 Conceptos nuevos
- U Endpoint: la pareja (verbo + ruta) que atiende un handler.
GET /tasksyPOST /tasksson dos endpoints distintos aunque compartan ruta. - U Path params (
/tasks/{id}) vs query params (/tasks?status=done): el path identifica qué recurso; el query modifica cómo se devuelve (filtros, orden, paginación).
Una query es la pregunta que le haces a la base de datos en SQL: SELECT * FROM tasks WHERE done = false. La BD traduce la pregunta a un plan de búsqueda — por eso los índices importan.
- U Idempotencia (primera aparición):
GET,PUT,DELETErepetidos dan el mismo resultado;POSTno — cada llamada crea otra tarea. Esto importa cuando el frontend reintenta. - TS Py Go Tres sintaxis de routing: cadena con
:id(Express), decorator + firma (FastAPI), patrón"GET /tasks/{id}"enServeMux(Go 1.22+).
Una firma (HMAC) prueba que un mensaje es auténtico y no fue tocado: se calcula con un secreto sobre el contenido. Los webhooks la usan para que verifiques “esto realmente vino de Stripe”.
12.4 La explicación visual
El router es un despachador: compara (método, path) contra la tabla de rutas registradas y entrega el request al handler correspondiente — o responde 404 él mismo si nadie reclamó la ruta.
12.5 Implementación
CRUD completo en memoria. El “modelo” es intencionalmente una lista — la base de datos llega en la Parte 2; hoy nos importa el contrato HTTP.
Un array es una colección ordenada de elementos accedidos por posición: ["a","b","c"][0] es "a" (se cuenta desde 0). Python las llama listas, Go slices — misma idea, distinto acento.
La memoria RAM es el escritorio de trabajo del programa: rápida, pero se borra al apagar. Los datos que deben sobrevivir van a disco o a la base de datos. Por eso let tasks = [] pierde todo al reiniciar.
Una base de datos es el programa que guarda datos de forma permanente y los responde rápido. Relacional (Postgres): tablas con relaciones. No relacional: documentos, clave-valor, grafos — cada una para una forma de dato distinta.
// Routing a mano: así se siente sin Express
import { createServer } from "node:http";
createServer((req, res) => {
const parts = req.url!.split("/"); // "/tasks/42" → ["","tasks","42"]
if (req.method === "GET" && parts[1] === "tasks" && !parts[2]) {
res.end(JSON.stringify(tasks));
} else if (req.method === "GET" && parts[1] === "tasks") {
const id = Number(parts[2]); // path param manual
// …find, 404, res.end…
}
// …un if anidado por cada (método, ruta)
}).listen(8000);Esto funciona — y es exactamente lo que Express abstrae: parsing de path params, dispatch por método, helpers de respuesta.
// src/index.ts — el canónico del libro
import express from "express";
const app = express();
app.use(express.json());
interface Task {
id: number;
title: string;
done: boolean;
}
const tasks: Task[] = [];
let nextId = 1;
app.get("/tasks", (_req, res) => {
res.json(tasks);
});
app.post("/tasks", (req, res) => {
const { title } = req.body ?? {};
if (typeof title !== "string" || !title.trim()) {
return res.status(400).json({ error: "title is required" });
}
const task: Task = { id: nextId++, title, done: false };
tasks.push(task);
res.status(201).json(task);
});
app.get("/tasks/:id", (req, res) => {
const task = tasks.find((t) => t.id === Number(req.params.id));
if (!task) return res.status(404).json({ error: "task not found" });
res.json(task);
});
app.delete("/tasks/:id", (req, res) => {
const i = tasks.findIndex((t) => t.id === Number(req.params.id));
if (i === -1) return res.status(404).json({ error: "task not found" });
tasks.splice(i, 1);
res.status(204).send();
});
app.listen(8000);// Hono — las mismas rutas, API casi idéntica, corre en edge/bun/deno
import { Hono } from "hono";
const app = new Hono();
app.get("/tasks", (c) => c.json(tasks));
app.get("/tasks/:id", (c) => {
const task = tasks.find((t) => t.id === Number(c.req.param("id")));
return task ? c.json(task) : c.json({ error: "not found" }, 404);
});Qué te cuesta: Hono/Fastify son swaps casi directos — mismo modelo, APIs más rápidas/modernas. NestJS es otro planeta: decoradores @Get("/tasks") sobre clases con DI — más estructura, más framework.
# Routing manual: lo que FastAPI hace por ti
from http.server import BaseHTTPRequestHandler
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/tasks":
... # listar
elif self.path.startswith("/tasks/"):
task_id = int(self.path.split("/")[2]) # path param a mano
...# app/main.py — el canónico del libro
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class Task(BaseModel):
id: int
title: str
done: bool = False
class TaskCreate(BaseModel):
title: str
tasks: list[Task] = []
next_id = 1
@app.get("/tasks")
def list_tasks() -> list[Task]:
return tasks
@app.post("/tasks", status_code=201)
def create_task(payload: TaskCreate) -> Task:
global next_id
task = Task(id=next_id, title=payload.title)
next_id += 1
tasks.append(task)
return task
@app.get("/tasks/{task_id}")
def get_task(task_id: int) -> Task:
for t in tasks:
if t.id == task_id:
return t
raise HTTPException(status_code=404, detail="task not found")
@app.delete("/tasks/{task_id}", status_code=204)
def delete_task(task_id: int):
for i, t in enumerate(tasks):
if t.id == task_id:
tasks.pop(i)
return
raise HTTPException(status_code=404, detail="task not found")# Flask — mismo mapa de rutas, validación manual
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.get("/tasks/<int:task_id>")
def get_task(task_id):
task = next((t for t in tasks if t["id"] == task_id), None)
return (jsonify(task), 200) if task else (jsonify({"error": "not found"}), 404)Qué te cuesta: Flask te da routing y JSON, pero la validación la escribes tú (no hay task_id: int que convierta+valide en la firma). Django REST Framework va al otro extremo: ViewSets + serializers — potencia total, framework entero.
// main.go
package main
import (
"encoding/json"
"net/http"
"strconv"
)
type Task struct {
ID int `json:"id"`
Title string `json:"title"`
Done bool `json:"done"`
}
var tasks []Task
var nextID = 1
func listTasks(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(tasks)
}
func createTask(w http.ResponseWriter, r *http.Request) {
var in struct{ Title string `json:"title"` }
if err := json.NewDecoder(r.Body).Decode(&in); err != nil || in.Title == "" {
http.Error(w, `{"error":"title is required"}`, http.StatusBadRequest)
return
}
t := Task{ID: nextID, Title: in.Title}
nextID++
tasks = append(tasks, t)
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(t)
}
func getTask(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(r.PathValue("id"))
for _, t := range tasks {
if t.ID == id {
json.NewEncoder(w).Encode(t)
return
}
}
http.Error(w, `{"error":"task not found"}`, http.StatusNotFound)
}
func deleteTask(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(r.PathValue("id"))
for i, t := range tasks {
if t.ID == id {
tasks = append(tasks[:i], tasks[i+1:]...)
w.WriteHeader(http.StatusNoContent)
return
}
}
http.Error(w, `{"error":"task not found"}`, http.StatusNotFound)
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /tasks", listTasks)
mux.HandleFunc("POST /tasks", createTask)
mux.HandleFunc("GET /tasks/{id}", getTask)
mux.HandleFunc("DELETE /tasks/{id}", deleteTask)
http.ListenAndServe(":8000", mux)
}// chi — mismos handlers http.HandlerFunc, router con más conveniencias
r := chi.NewRouter()
r.Get("/tasks", listTasks)
r.Get("/tasks/{id}", getTask) // chi.URLParam(r, "id")
// Gin — framework completo: binding, JSON helpers, middleware built-in
router := gin.Default()
router.GET("/tasks/:id", func(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
// c.JSON(200, task) / c.AbortWithStatusJSON(404, …)
})Qué te cuesta: chi es stdlib-friendly y casi gratis. Gin/Echo dan contextos propios (*gin.Context en vez de (w, r)) — cómodos pero te atan a su API. En Go muchas APIs serias viven solo con ServeMux.
Así se prueba cualquiera de los tres — misma llamada, mismo resultado:
curl -i -X POST http://localhost:8000/tasks \
-H "Content-Type: application/json" -d '{"title":"ship v1"}'
# HTTP/1.1 201 Created
# {"id":1,"title":"ship v1","done":false}
curl -i http://localhost:8000/tasks/99
# HTTP/1.1 404 Not Found
# {"error":"task not found"}Implementa un CRUD en memoria para una API de tareas en
[Express+TypeScript / FastAPI+Python / net/http+Go]. Endpoints:
GET /tasks (lista), POST /tasks (crear, body {title}), GET /tasks/{id}
(una, 404 si no existe), DELETE /tasks/{id} (204 o 404). Requisitos:
status codes correctos (201, 204, 404), sin base de datos (array en
memoria), y los comandos curl para probar cada endpoint. Después
explícame qué convención REST aplica cada ruta.
12.6 ¿Por qué cada stack lo hace así?
Lo único que necesitas llevarte: las rutas, verbos y status codes son idénticos en los tres — eso es HTTP, no el lenguaje. Lo que cambia es cuánto trabajo te quita el framework encima. El detalle:
Un framework es un esqueleto de aplicación ya decidido: te da la estructura (rutas, validación, errores) y tú llenas la lógica. Diferencia con librería: la librería la llamas tú; el framework te llama a ti.
- Express es “Imperativo minimalista”: tú parseas el body (
express.json()), tú conviertesreq.params.idde string a número, tú eligesres.status(201). Libertad total = responsabilidad total. - FastAPI hace lo contrario: declaras
task_id: inten la firma y el framework convierte y valida — si llega/tasks/abc, responde422antes de tocar tu función. Menos código, más “magia” (que el Cap. 16 desmontará). - Go stdlib es explícito al máximo:
r.PathValue("id")devuelve string y tú la conviertes;http.Errorescribe la respuesta de error a mano. Cero magia, cero sorpresas, más líneas.
Nótese lo que es idéntico en los tres: la forma de las rutas, los verbos, los status codes. Eso es el protocolo HTTP — el lenguaje solo cambia cómo lo expresas.
Esta decisión es material: algún mapa verbo+ruta→handler debe existir (estructural), pero las convenciones de diseño son elegibles — REST es una cimentación popular, no la única.
| Alternativa | Qué es | Qué te cuesta | Cuándo elegirla |
|---|---|---|---|
| REST de recursos (este libro) | Sustantivos + verbos HTTP + status | Diseñar recursos bien; no todo encaja en CRUD | APIs públicas, equipos diversos — el default |
RPC-style (/getTask, /doAction) |
Funciones remotas por nombre | Pierdes convenciones (caché, idempotencia) que HTTP regala | APIs internas, operaciones que no son recursos |
| GraphQL | Un endpoint, el cliente pide campos | Otra capa entera: schema, resolvers, N+1 | Clientes con necesidades de datos muy distintas |
| tRPC / OpenAPI codegen | Contrato tipado end-to-end | Acopla cliente y servidor al mismo lenguaje/schema | Monorepos TS, equipos full-stack |
El libro usa REST porque sus convenciones (status codes, idempotencia) son lecciones gratis de diseño — y porque es lo que el 90% de los equipos espera ver.
12.7 Errores comunes
- Responder siempre 200.
200 {"error":"not found"}obliga al cliente a parsear el body para saber si falló. El status code existe para eso. - Verbos en la URL (
/getTask/42,/deleteTask/42): duplica lo que el método ya dice y rompe las convenciones que caches y herramientas asumen.
Un caché es una copia rápida de algo costoso de obtener: el resultado de una query pesada, la sesión. Redis es el caché estándar. La regla de oro: cachear es fácil, invalidar (saber cuándo el caché ya no vale) es lo difícil.
- Olvidar
express.json()(o equivalente):req.bodyllegaundefinedy pasas 20 minutos creyendo que el cliente manda mal. - Asumir que
{id}es número: en Express y Go llega string —Number("abc")esNaN,Atoi("abc")es error. FastAPI lo resuelve por ti; en los otros es tu trabajo (hasta el capítulo de validación).
12.8 Buenas prácticas
- Tabla de rutas antes que código: la tabla de arriba es el diseño.
201+ el recurso creado en el body — el frontend no necesita un segundoGETpara pintar la tarea.204para DELETE exitoso: “salió bien, no hay contenido” es más honesto que devolver{}.- Query params para filtros (
/tasks?done=false), path params para identidad (/tasks/42). Mezclarlos confunde el contrato.
EXTRA El libro usa Express, FastAPI y Go estándar, pero el ecosistema real es más amplio. Roadmaps externos recomiendan conocer —
- Ruby on Rails — “convención sobre configuración”, testing integrado, localización; open source escrito en Ruby
- Django (Python) — “baterías incluidas”: auth, admin, ORM; hoy también async
- Flask (Python) — microframework minimalista, servidor de desarrollo integrado, rápido para prototipar
- Express (Node) — ligero y adaptable; depuración y desarrollo de servidores rápidos
Mismo patrón en todos: rutas → handlers → middleware. Si entiendes el de este libro, lees cualquiera de estos en una tarde.
12.9 Ejercicio
- Implementa el CRUD en tu stack y pruébalo con
curlo Thunder Client: crear dos tareas, listar, obtener una, borrar otra, verificar el 404. - Agrega
PATCH /tasks/{id}que solo permita cambiardone. - Agrega
GET /tasks?done=trueque filtre por query param.
12.10 Mini reto
Diseña las rutas de una API de “likes” sobre tareas: un usuario puede dar like a una tarea, quitarlo, y ver quiénes dieron like. Sin verbos en la URL y sin ambigüedad — escribe la tabla antes de pensar en código.
Ejercicio 2 (PATCH). Solo se acepta done:
@app.patch("/tasks/{task_id}")
def update_task(task_id: int, payload: TaskUpdate):
task = find_or_404(task_id)
if payload.done is not None:
task.done = payload.done
return task
# TaskUpdate = BaseModel con done: bool | None = NoneEjercicio 3 (filtro): GET /tasks?done=true → [t for t in tasks if t.done == done] cuando el query param está presente.
Mini reto. Tabla modelo:
| Verbo + ruta | Significado | Éxito |
|---|---|---|
PUT /tasks/{id}/like |
dar like (idempotente: repetir no duplica) | 204 |
DELETE /tasks/{id}/like |
quitar like | 204 |
GET /tasks/{id}/likes |
quiénes dieron like | 200 + lista |
La decisión clave: PUT en vez de POST para dar like — como un usuario solo puede dar like una vez, la operación es idempotente y PUT lo expresa.
12.11 Vocabulario técnico del capítulo
Una variable es una caja con nombre donde guardas un valor: let total = 42 guarda el 42 bajo el nombre total. Puedes leerla y cambiarla después (total = 50). const = caja que no se puede reemplazar.
Una función es una receta reutilizable: recibe ingredientes (parámetros), hace pasos y devuelve un plato (return). La escribes una vez y la llamas mil veces: add(2, 3) → 5.
Un objeto es una colección de datos con nombre: {name: "Ana", age: 30} — cada dato es una propiedad (clave → valor). Python los llama dict, Go los arma con struct, TS con objetos/interface.
Un bucle repite una acción por cada elemento o hasta cumplir una condición: for task in tasks hace algo con cada tarea. map y filter son bucles disfrazados de funciones — transforman/filtran colecciones sin for explícito.
Una clase es el molde de un objeto: define qué datos y qué métodos tiene. class Task es el molde; new Task() es una instancia concreta. Go no tiene clases — usa struct + métodos sueltos.
Un string es texto entre comillas: "hola". El nombre viene de “cadena de caracteres” — una secuencia de letras. Todo lo que llega de un formulario o una URL llega como string, aunque parezca número.
Un booleano es un valor de dos estados: true o false. Es el resultado de toda comparación (age > 18) y lo que los if evalúan. Nombrado por George Boole, el matemático de la lógica.
null (TS), None (Py), nil (Go) = “aquí no hay valor”. Es la respuesta a “¿qué devuelvo cuando no hay nada?” — y la fuente del bug más famoso de la historia (su inventor lo llamó “el error del billón de dólares”). Por eso el código revisa if x is not None.
localhost significa “esta misma máquina” — es la dirección que tu computadora usa para hablarse a sí misma. Cuando desarrollas, el “servidor” y el “cliente” viven en tu laptop: por eso todo es localhost:3000.
El esquema es el plano de la base de datos: qué tablas hay, qué columnas tiene cada una y de qué tipo. Es contrato: una fila que no cumple el esquema no entra. Se cambia con migraciones, no a mano.
Un middleware es un filtro en la cadena del request: pasa por él antes de llegar a tu handler. Auth es middleware — “verifica el token” vive una vez y protege todas las rutas, no se copia en cada una.
Un servidor es una computadora que espera peticiones y las responde 24/7. Físicamente no es nada mágico: es una máquina (a veces una VM alquilada) corriendo tu programa, con la diferencia de que está siempre encendida y conectada.