Parte 4: Respuestas Avanzadas¶
Estado verificado al 28 de marzo de 2026. Nota de runtime: FastFN auto-instala dependencias locales por función desde
requirements.txt/package.json; enfastfn dev --nativenecesitas runtimes instalados en host, mientras quefastfn devdepende de Docker daemon activo.
Vista rápida¶
- Complejidad: Intermedio
- Tiempo típico: 30-40 minutos
- Resultado: contratos estables de respuesta con comportamiento multi-status explícito
1. Garantía de forma de respuesta¶
Usa envelope explícito:
{
"status": 200,
"headers": {"Content-Type": "application/json; charset=utf-8"},
"body": {"data": {}, "error": null, "meta": {}}
}
2. Modelos alternativos según estado¶
Elige un runtime para functions/tasks/[id]/get.*:
use serde_json::{json, Value};
pub fn handler(_event: Value, params: Value) -> Value {
let id = params.get("id").and_then(|v| v.as_str()).unwrap_or("");
if id == "404" {
return json!({"status": 404, "body": {"error": {"code": "TASK_NOT_FOUND", "message": "task not found"}}});
}
json!({"status": 200, "body": {"data": {"id": id, "title": "Escribir docs"}, "error": null}})
}
<?php
function handler(array $event, array $params): array {
$id = $params['id'] ?? '';
if ($id === '404') {
return ['status' => 404, 'body' => ['error' => ['code' => 'TASK_NOT_FOUND', 'message' => 'task not found']]];
}
return ['status' => 200, 'body' => ['data' => ['id' => $id, 'title' => 'Escribir docs'], 'error' => null]];
}
Curls por runtime:
3. Estrategia de códigos de estado¶
| Estado | Cuándo usar | Contrato |
|---|---|---|
200 |
lectura/actualización ok | data presente, error: null |
201 |
recurso creado | data con id |
202 |
trabajo async aceptado | job_id + URL de consulta |
400 |
request malformado | error accionable |
404 |
recurso/ruta inexistente | error + mensaje |
409 |
conflicto | detalle de conflicto |
422 |
validación semántica | mensaje por campo |
4. Códigos adicionales en un endpoint¶
Elige runtime para functions/tasks/post.*:
exports.handler = async (event) => {
const body = JSON.parse(event.body || "{}");
if (!body.title) return { status: 422, body: { error: "title requerido" } };
if (body.async === true) return { status: 202, body: { job_id: "job-123", status_url: "/_fn/jobs/job-123" } };
return { status: 201, body: { id: 99, title: body.title } };
};
import json
def handler(event):
body = json.loads(event.get("body") or "{}")
if not body.get("title"):
return {"status": 422, "body": {"error": "title requerido"}}
if body.get("async") is True:
return {"status": 202, "body": {"job_id": "job-123", "status_url": "/_fn/jobs/job-123"}}
return {"status": 201, "body": {"id": 99, "title": body["title"]}}
use serde_json::{json, Value};
pub fn handler(event: Value) -> Value {
let parsed: Value = serde_json::from_str(event.get("body").and_then(|x| x.as_str()).unwrap_or("{}")).unwrap_or(json!({}));
if parsed.get("title").and_then(|x| x.as_str()).unwrap_or("").is_empty() {
return json!({"status": 422, "body": {"error": "title requerido"}});
}
if parsed.get("async").and_then(|x| x.as_bool()).unwrap_or(false) {
return json!({"status": 202, "body": {"job_id": "job-123", "status_url": "/_fn/jobs/job-123"}});
}
json!({"status": 201, "body": {"id": 99, "title": parsed["title"]}})
}
<?php
function handler(array $event): array {
$body = json_decode($event['body'] ?? '{}', true) ?: [];
if (empty($body['title'])) return ['status' => 422, 'body' => ['error' => 'title requerido']];
if (($body['async'] ?? false) === true) {
return ['status' => 202, 'body' => ['job_id' => 'job-123', 'status_url' => '/_fn/jobs/job-123']];
}
return ['status' => 201, 'body' => ['id' => 99, 'title' => $body['title']]];
}
Curls por runtime:
curl -sS -X POST 'http://127.0.0.1:8080/tasks' -H 'Content-Type: application/json' -d '{}'
curl -sS -X POST 'http://127.0.0.1:8080/tasks' -H 'Content-Type: application/json' -d '{"title":"Docs","async":true}'
curl -sS -X POST 'http://127.0.0.1:8080/tasks' -H 'Content-Type: application/json' -d '{"title":"Docs"}'
curl -sS -X POST 'http://127.0.0.1:8080/tasks' -H 'Content-Type: application/json' -d '{}'
curl -sS -X POST 'http://127.0.0.1:8080/tasks' -H 'Content-Type: application/json' -d '{"title":"Docs","async":true}'
curl -sS -X POST 'http://127.0.0.1:8080/tasks' -H 'Content-Type: application/json' -d '{"title":"Docs"}'
curl -sS -X POST 'http://127.0.0.1:8080/tasks' -H 'Content-Type: application/json' -d '{}'
curl -sS -X POST 'http://127.0.0.1:8080/tasks' -H 'Content-Type: application/json' -d '{"title":"Docs","async":true}'
curl -sS -X POST 'http://127.0.0.1:8080/tasks' -H 'Content-Type: application/json' -d '{"title":"Docs"}'
curl -sS -X POST 'http://127.0.0.1:8080/tasks' -H 'Content-Type: application/json' -d '{}'
curl -sS -X POST 'http://127.0.0.1:8080/tasks' -H 'Content-Type: application/json' -d '{"title":"Docs","async":true}'
curl -sS -X POST 'http://127.0.0.1:8080/tasks' -H 'Content-Type: application/json' -d '{"title":"Docs"}'
5. Envelope de errores¶
{
"error": {
"code": "VALIDATION_ERROR",
"message": "title requerido",
"hint": "enviar title como string no vacío"
}
}
6. Actualizaciones de body (PUT vs PATCH)¶
Usa PUT para reemplazo completo y PATCH para merge parcial.
exports.handler = async (event, params) => {
const id = params.id;
const body = JSON.parse(event.body || "{}");
if (event.method === "PUT") {
return { status: 200, body: { id, title: body.title || "", done: !!body.done } };
}
if (event.method === "PATCH") {
const current = { id, title: "Titulo actual", done: false };
return { status: 200, body: { ...current, ...body, id } };
}
return { status: 405, body: { error: "method not allowed" } };
};
import json
def handler(event, params):
item_id = params.get("id")
body = json.loads(event.get("body") or "{}")
method = (event.get("method") or "").upper()
if method == "PUT":
return {"status": 200, "body": {"id": item_id, "title": body.get("title", ""), "done": bool(body.get("done"))}}
if method == "PATCH":
current = {"id": item_id, "title": "Titulo actual", "done": False}
current.update(body)
current["id"] = item_id
return {"status": 200, "body": current}
return {"status": 405, "body": {"error": "method not allowed"}}
use serde_json::{json, Value};
pub fn handler(event: Value, params: Value) -> Value {
let id = params.get("id").and_then(|v| v.as_str()).unwrap_or("");
let method = event.get("method").and_then(|v| v.as_str()).unwrap_or("");
let body: Value = serde_json::from_str(event.get("body").and_then(|v| v.as_str()).unwrap_or("{}")).unwrap_or(json!({}));
if method.eq_ignore_ascii_case("PUT") {
return json!({"status": 200, "body": {"id": id, "title": body.get("title").and_then(|v| v.as_str()).unwrap_or(""), "done": body.get("done").and_then(|v| v.as_bool()).unwrap_or(false)}});
}
if method.eq_ignore_ascii_case("PATCH") {
let mut current = json!({"id": id, "title": "Titulo actual", "done": false});
if let Some(obj) = body.as_object() {
for (k, v) in obj {
current[k] = v.clone();
}
}
current["id"] = json!(id);
return json!({"status": 200, "body": current});
}
json!({"status": 405, "body": {"error": "method not allowed"}})
}
<?php
function handler(array $event, array $params): array {
$id = $params['id'] ?? '';
$body = json_decode($event['body'] ?? '{}', true) ?: [];
$method = strtoupper($event['method'] ?? '');
if ($method === 'PUT') {
return ['status' => 200, 'body' => ['id' => $id, 'title' => $body['title'] ?? '', 'done' => (bool)($body['done'] ?? false)]];
}
if ($method === 'PATCH') {
$current = ['id' => $id, 'title' => 'Titulo actual', 'done' => false];
$merged = array_merge($current, $body);
$merged['id'] = $id;
return ['status' => 200, 'body' => $merged];
}
return ['status' => 405, 'body' => ['error' => 'method not allowed']];
}
curl -sS -X PUT 'http://127.0.0.1:8080/tasks/9' -H 'Content-Type: application/json' -d '{"title":"Reemplazo","done":true}'
curl -sS -X PATCH 'http://127.0.0.1:8080/tasks/9' -H 'Content-Type: application/json' -d '{"done":true}'
7. Responder directamente¶
Puedes devolver una respuesta armada sin helpers:
Útil para deletes idempotentes sin contenido.
8. Respuesta custom y tipo de contenido¶
Usa text/html, text/csv u otros tipos cuando no sea JSON:
Headers esperados:
Content-Type: text/html; charset=utf-8Content-Type: text/csv; charset=utf-8Cache-Controlcuando aplique cache
9. Respuestas adicionales en OpenAPI¶
Si una función retorna varios estados (200, 404, 409), documenta y valida en OpenAPI:
10. Cookies de respuesta¶
Set/Clear explícito:
Flags recomendadas en producción:
HttpOnlySecureSameSite=Lax(oStricten backoffice sensible)
11. Headers de respuesta¶
Headers operativos útiles:
X-Request-IdX-Trace-SourceCache-ControlETag
12. Cambio dinámico de status¶
El estado puede variar según resultado real:
200siPUT /tasks/:idactualizó un registro existente201siPUT /tasks/:idcreó un registro nuevo202si encoló async
curl -i -X PUT 'http://127.0.0.1:8080/tasks/1' -H 'Content-Type: application/json' -d '{"title":"a"}'
Validación¶
GET /tasks/:iddevuelve200y404con envelope consistente.POST /tasksdevuelve201,202o422según payload.PUT/PATCHtienen semántica clara y estable.openapi.jsonrefleja las respuestas alternativas.
Troubleshooting¶
- Si hay mismatch de status/body, valida que el handler siempre devuelva body-objeto o JSON string de forma consistente.
- Si cookies no aparecen en navegador, revisa
Secure+SameSitey si estás en HTTPS/localhost. - Si OpenAPI no refleja respuestas alternativas, re-ejecuta discovery y verifica metadata de rutas.