Saltar al contenido
Lección 17 de 22

Proyecto: API e Integraciones

16 min read

Descripcion General del Proyecto

FlowTask es una app SaaS funcional, pero ahora mismo es un jardin amurallado. Ninguna otra aplicacion puede leer o escribir datos en ella. Ningun servicio externo recibe notificaciones cuando algo sucede. Eso limita su utilidad drasticamente.

En esta leccion, vamos a extender FlowTask con tres capacidades que la transforman de una app independiente en una plataforma:

  1. Una API REST publica que permite a otras aplicaciones crear, leer, actualizar y eliminar tareas y proyectos programaticamente
  2. Webhooks que notifican a servicios externos cuando ocurren eventos dentro de FlowTask
  3. Integraciones con servicios del mundo real como Stripe para pagos y Resend para email transaccional

Al final de esta leccion, FlowTask tendra autenticacion por API key, rate limiting, documentacion autogenerada, entrega de webhooks con logica de reintentos, procesamiento de pagos y notificaciones por email. Estos son los bloques fundamentales que convierten un proyecto personal en un negocio real.

Disenando una API Publica

Una buena API es predecible, consistente y bien documentada. Antes de escribir cualquier codigo, planifiquemos el diseno.

Convenciones RESTful

Nuestra API seguira las convenciones REST:

  • Sustantivos, no verbos en las URLs: /api/v1/tasks, no /api/v1/getTasks
  • Metodos HTTP expresan la accion: GET lee, POST crea, PUT actualiza, DELETE elimina
  • Versionado con un prefijo en la ruta: /api/v1/ para poder lanzar v2 sin romper integraciones existentes
  • Formato de respuesta consistente para cada endpoint

Formato de Respuesta

Cada respuesta sigue la misma estructura:

{
  "data": { ... },
  "meta": {
    "page": 1,
    "perPage": 20,
    "total": 47
  },
  "error": null
}

Para errores:

{
  "data": null,
  "meta": null,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Title is required",
    "details": [
      { "field": "title", "message": "This field is required" }
    ]
  }
}

Plan de Endpoints

Esta es la lista completa de endpoints:

| Metodo | Endpoint | Descripcion | |--------|----------|-------------| | GET | /api/v1/tasks | Listar tareas con paginacion y filtrado | | GET | /api/v1/tasks/:id | Obtener una tarea individual | | POST | /api/v1/tasks | Crear una tarea | | PUT | /api/v1/tasks/:id | Actualizar una tarea | | DELETE | /api/v1/tasks/:id | Eliminar una tarea | | GET | /api/v1/projects | Listar proyectos | | GET | /api/v1/projects/:id | Obtener un proyecto individual | | POST | /api/v1/projects | Crear un proyecto | | PUT | /api/v1/projects/:id | Actualizar un proyecto | | DELETE | /api/v1/projects/:id | Eliminar un proyecto |

Construyendo los Endpoints de la API

Construyamos los endpoints de tareas. Los endpoints de proyectos siguen el mismo patron.

El Prompt

Build the public API for FlowTask. Create these endpoint files under
app/api/v1/:

1. tasks/route.ts — handles GET (list) and POST (create)
2. tasks/[id]/route.ts — handles GET (single), PUT (update), DELETE

Requirements:
- Authenticate using an API key in the Authorization header (Bearer scheme)
- Validate request bodies using zod schemas
- Return consistent JSON responses with data/meta/error shape
- Support pagination on list endpoints: ?page=1&perPage=20
- Support filtering tasks by status, priority, and projectId
- Return proper HTTP status codes: 200 OK, 201 Created, 204 No Content,
  400 Bad Request, 401 Unauthorized, 404 Not Found, 500 Internal Server Error
- Include TypeScript types for all request/response shapes

Create a shared utility function for the response format.

GET /api/v1/tasks

El endpoint de listado soporta paginacion y filtrado:

// app/api/v1/tasks/route.ts
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { authenticateApiKey } from "@/lib/api-auth";
import { apiResponse, apiError } from "@/lib/api-response";

export async function GET(request: NextRequest) {
  const user = await authenticateApiKey(request);
  if (!user) return apiError("Unauthorized", 401);

  const { searchParams } = new URL(request.url);
  const page = parseInt(searchParams.get("page") || "1");
  const perPage = parseInt(searchParams.get("perPage") || "20");
  const status = searchParams.get("status");
  const priority = searchParams.get("priority");
  const projectId = searchParams.get("projectId");

  const where = {
    project: { userId: user.id },
    ...(status && { status }),
    ...(priority && { priority }),
    ...(projectId && { projectId }),
  };

  const [tasks, total] = await Promise.all([
    prisma.task.findMany({
      where,
      skip: (page - 1) * perPage,
      take: perPage,
      orderBy: { createdAt: "desc" },
      include: { project: { select: { id: true, name: true } } },
    }),
    prisma.task.count({ where }),
  ]);

  return apiResponse(tasks, { page, perPage, total });
}

POST /api/v1/tasks

El endpoint de creacion valida el cuerpo de la solicitud:

export async function POST(request: NextRequest) {
  const user = await authenticateApiKey(request);
  if (!user) return apiError("Unauthorized", 401);

  const body = await request.json();
  const parsed = createTaskSchema.safeParse(body);
  if (!parsed.success) {
    return apiError("Validation failed", 400, parsed.error.issues);
  }

  const { title, description, status, priority, dueDate, projectId } = parsed.data;

  // Verificar que el proyecto pertenece al usuario
  const project = await prisma.project.findFirst({
    where: { id: projectId, userId: user.id },
  });
  if (!project) return apiError("Project not found", 404);

  const task = await prisma.task.create({
    data: { title, description, status, priority, dueDate, projectId },
  });

  return apiResponse(task, null, 201);
}

Codigos de Estado HTTP Correctos

Cada endpoint devuelve el codigo de estado apropiado:

  • 200 --- GET o PUT exitoso
  • 201 --- POST exitoso (recurso creado)
  • 204 --- DELETE exitoso (sin contenido que devolver)
  • 400 --- Solicitud incorrecta (errores de validacion, JSON malformado)
  • 401 --- No autorizado (API key faltante o invalida)
  • 404 --- Recurso no encontrado
  • 429 --- Limite de tasa excedido
  • 500 --- Error interno del servidor

Estas convenciones hacen tu API predecible. Cualquier desarrollador integrándose con FlowTask sabe exactamente que esperar.

Autenticacion de la API

La aplicacion web usa autenticacion basada en sesiones via NextAuth. La API necesita un mecanismo diferente: API keys.

El Prompt

Implement API key authentication for FlowTask:

1. Add an ApiKey model to Prisma: id, key (unique, indexed), name,
   userId (foreign key), lastUsedAt, createdAt. The key should be a
   random 32-character hex string prefixed with "ft_".

2. Create a settings page section for API keys: list existing keys,
   create new key (show once then mask), delete key.

3. Create an authenticateApiKey middleware function that:
   - Extracts the key from the Authorization: Bearer header
   - Looks up the key in the database
   - Updates lastUsedAt
   - Returns the associated user or null

4. Run the migration.

Generacion de Keys

Las API keys deben ser criptograficamente aleatorias:

import { randomBytes } from "crypto";

function generateApiKey(): string {
  return `ft_${randomBytes(32).toString("hex")}`;
}

El prefijo ft_ ayuda a los usuarios a identificar a que servicio pertenece una key. Este es un detalle pequeno pero pensado que mejora la experiencia del desarrollador.

Consideraciones de Seguridad

Cuando la IA genere el flujo de creacion de API keys, verifica estas practicas de seguridad:

  • Las keys se hashean antes de almacenarse (o como minimo, la key completa se muestra solo una vez al crearla)
  • Las keys pueden revocarse individualmente
  • Cada key tiene un nombre para que los usuarios identifiquen que integracion usa cada key
  • El timestamp lastUsedAt ayuda a los usuarios a identificar keys inactivas para revocar

Rate Limiting

Sin rate limiting, un solo consumidor de API podria sobrecargar tu servidor. El rate limiting protege tu infraestructura y asegura un uso justo.

El Prompt

Add rate limiting to the FlowTask API:

1. Implement a sliding window rate limiter using an in-memory store (Map).
   For production, note that this should use Redis.

2. Rate limits:
   - Default: 100 requests per minute per API key
   - Create/update/delete endpoints: 30 requests per minute per API key

3. Add rate limit headers to every API response:
   - X-RateLimit-Limit: the maximum requests allowed
   - X-RateLimit-Remaining: requests left in the current window
   - X-RateLimit-Reset: Unix timestamp when the window resets

4. Return 429 Too Many Requests when the limit is exceeded, with a
   Retry-After header.

Create the rate limiter as middleware that wraps the API route handlers.

El Rate Limiter

La implementacion de ventana deslizante rastrea timestamps de solicitudes:

// lib/rate-limiter.ts
const requests = new Map<string, number[]>();

export function checkRateLimit(
  key: string,
  limit: number,
  windowMs: number = 60000
): { allowed: boolean; remaining: number; resetAt: number } {
  const now = Date.now();
  const windowStart = now - windowMs;

  const timestamps = (requests.get(key) || []).filter((t) => t > windowStart);
  timestamps.push(now);
  requests.set(key, timestamps);

  const remaining = Math.max(0, limit - timestamps.length);
  const resetAt = Math.ceil((windowStart + windowMs) / 1000);

  return {
    allowed: timestamps.length <= limit,
    remaining,
    resetAt,
  };
}

Para produccion, reemplaza el Map en memoria con Redis usando ZRANGEBYSCORE para la ventana deslizante. El prompt para hacer ese cambio:

Replace the in-memory rate limiter with Redis. Use the ioredis package.
Use a sorted set per API key with timestamps as scores. ZRANGEBYSCORE
to count requests in the current window. ZADD to record new requests.
ZREMRANGEBYSCORE to clean up expired entries.

Documentacion de API Autogenerada

Una buena documentacion es la diferencia entre una API que la gente usa y una que abandonan. La mejor documentacion se genera desde tu codigo para que nunca se desincronice.

El Prompt

Generate API documentation for FlowTask:

1. Create an OpenAPI 3.0 specification (openapi.json) that describes all
   endpoints, request/response schemas, authentication, and error formats.

2. Create an interactive documentation page at /docs/api using Swagger UI
   or a similar renderer. The page should let developers try out API calls
   directly from the browser.

3. Include examples for every endpoint showing the request and response.

4. Document the authentication flow: how to get an API key, how to include
   it in requests.

5. Document rate limiting: headers, limits, and what to do when rate limited.

La especificacion OpenAPI define tu API en un formato legible por maquinas:

{
  "openapi": "3.0.0",
  "info": {
    "title": "FlowTask API",
    "version": "1.0.0",
    "description": "Public API for managing FlowTask projects and tasks"
  },
  "servers": [
    { "url": "https://flowtask.app/api/v1" }
  ],
  "paths": {
    "/tasks": {
      "get": {
        "summary": "List tasks",
        "parameters": [
          { "name": "page", "in": "query", "schema": { "type": "integer" } },
          { "name": "status", "in": "query", "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "List of tasks" }
        }
      }
    }
  }
}

Los docs interactivos permiten a los desarrolladores probar tu API sin escribir codigo. Llenan los parametros, hacen click en "Try it out" y ven la respuesta. Esto reduce la friccion enormemente.

Webhooks: Enviando Eventos

Los webhooks permiten a FlowTask notificar a servicios externos cuando suceden cosas. En lugar de que otras apps consulten tu API cada pocos segundos preguntando "cambio algo?", tu les envias actualizaciones en tiempo real.

El Prompt

Implement outgoing webhooks for FlowTask:

1. Add a Webhook model to Prisma: id, url (string), secret (string for
   HMAC signing), events (string array of event types), userId, active
   (boolean), createdAt.

2. Add webhook management to the settings page: register a webhook URL,
   select which events to subscribe to, test webhook delivery, delete
   webhook.

3. Event types: task.created, task.updated, task.completed, task.deleted,
   project.created, project.deleted

4. Create a dispatchWebhook utility that:
   - Finds all active webhooks for the user subscribed to the event type
   - Sends a POST request to each webhook URL with the event payload
   - Signs the payload with HMAC-SHA256 using the webhook secret
   - Includes headers: X-FlowTask-Event, X-FlowTask-Signature,
     X-FlowTask-Delivery (unique ID)
   - Implements retry logic: retry 3 times with exponential backoff
     (1s, 5s, 25s) on failure

5. Call dispatchWebhook after every relevant Server Action and API mutation.

Run the migration.

Payload del Webhook

Cada entrega de webhook incluye un payload firmado:

// lib/webhooks.ts
import crypto from "crypto";

interface WebhookPayload {
  event: string;
  data: Record<string, unknown>;
  timestamp: string;
  deliveryId: string;
}

function signPayload(payload: string, secret: string): string {
  return crypto
    .createHmac("sha256", secret)
    .update(payload)
    .digest("hex");
}

export async function dispatchWebhook(
  userId: string,
  event: string,
  data: Record<string, unknown>
) {
  const webhooks = await prisma.webhook.findMany({
    where: {
      userId,
      active: true,
      events: { has: event },
    },
  });

  const payload: WebhookPayload = {
    event,
    data,
    timestamp: new Date().toISOString(),
    deliveryId: crypto.randomUUID(),
  };

  const body = JSON.stringify(payload);

  for (const webhook of webhooks) {
    const signature = signPayload(body, webhook.secret);
    await deliverWithRetry(webhook.url, body, {
      "Content-Type": "application/json",
      "X-FlowTask-Event": event,
      "X-FlowTask-Signature": `sha256=${signature}`,
      "X-FlowTask-Delivery": payload.deliveryId,
    });
  }
}

Logica de Reintentos

Las solicitudes de red fallan. La entrega de webhooks necesita logica de reintentos:

async function deliverWithRetry(
  url: string,
  body: string,
  headers: Record<string, string>,
  maxRetries: number = 3
) {
  const delays = [1000, 5000, 25000]; // Backoff exponencial

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url, {
        method: "POST",
        headers,
        body,
        signal: AbortSignal.timeout(10000), // Timeout de 10s
      });

      if (response.ok) return;
      if (response.status < 500) return; // No reintentar errores del cliente
    } catch {
      // Error de red, se reintentara
    }

    if (attempt < maxRetries) {
      await new Promise((resolve) => setTimeout(resolve, delays[attempt]));
    }
  }
}

Webhooks: Recibiendo Eventos

FlowTask envia webhooks, pero tambien necesita recibirlos de servicios externos. El caso mas comun: webhooks de pago de Stripe.

Construyendo un Receptor de Webhooks

Create a webhook receiver endpoint at /api/webhooks/stripe that:

1. Reads the raw request body (don't parse JSON yet)
2. Verifies the Stripe signature using the stripe.webhooks.constructEvent
   method with the STRIPE_WEBHOOK_SECRET environment variable
3. Handles these event types:
   - checkout.session.completed — activate the user's subscription
   - customer.subscription.updated — update subscription status
   - customer.subscription.deleted — cancel the subscription
4. Returns 200 OK immediately, processes the event asynchronously
5. Logs all webhook events for debugging

Verificando Firmas

Nunca confies en un payload de webhook sin verificar la firma. Cualquiera podria enviar un POST a tu URL de webhook pretendiendo ser Stripe:

import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(request: NextRequest) {
  const body = await request.text();
  const signature = request.headers.get("stripe-signature")!;

  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch {
    return new NextResponse("Invalid signature", { status: 400 });
  }

  // Firma verificada — seguro para procesar
  switch (event.type) {
    case "checkout.session.completed":
      await handleCheckoutCompleted(event.data.object);
      break;
    case "customer.subscription.deleted":
      await handleSubscriptionCanceled(event.data.object);
      break;
  }

  return new NextResponse("OK", { status: 200 });
}

El patron siempre es el mismo: leer el body crudo, verificar la firma, procesar el evento, devolver 200. Devuelve 200 rapidamente --- si tu handler tarda demasiado, el emisor del webhook pensara que fallo y reintentara.

Integrando APIs Externas

Conectemos FlowTask con servicios reales.

Stripe para Pagos

Integrate Stripe payments into FlowTask:

1. Install the stripe package and @stripe/stripe-js for the frontend
2. Create a Subscription model: id, userId, stripeCustomerId,
   stripeSubscriptionId, plan (FREE, PRO, ENTERPRISE), status, currentPeriodEnd
3. Create a checkout API route that creates a Stripe Checkout Session
   for Pro or Enterprise plans
4. After successful payment (via webhook), update the user's subscription
5. Add a billing section to the settings page showing current plan, next
   billing date, and an "Upgrade" or "Manage Subscription" button
6. Use Stripe's customer portal for subscription management

Run the migration.

El flujo de checkout envia a los usuarios a la pagina de pago alojada de Stripe, que maneja todo el procesamiento sensible de tarjetas de credito. Tu nunca tocas numeros de tarjeta:

export async function POST(request: NextRequest) {
  const user = await getCurrentUser();
  const { plan } = await request.json();

  const priceId =
    plan === "PRO"
      ? process.env.STRIPE_PRO_PRICE_ID
      : process.env.STRIPE_ENTERPRISE_PRICE_ID;

  const session = await stripe.checkout.sessions.create({
    customer_email: user.email,
    line_items: [{ price: priceId, quantity: 1 }],
    mode: "subscription",
    success_url: `${process.env.NEXTAUTH_URL}/settings/billing?success=true`,
    cancel_url: `${process.env.NEXTAUTH_URL}/settings/billing?canceled=true`,
    metadata: { userId: user.id },
  });

  return NextResponse.json({ url: session.url });
}

Resend para Notificaciones por Email

Integrate Resend for transactional emails:

1. Install the resend package
2. Create an email utility in lib/email.ts with functions for:
   - sendTaskAssignedEmail(task, assignee)
   - sendTaskCompletedEmail(task, projectOwner)
   - sendWeeklyDigestEmail(user, stats)
3. Create email templates as React components using @react-email/components
4. Call the email functions from the appropriate Server Actions and
   webhook handlers
5. Use the RESEND_API_KEY environment variable

La integracion con Resend es directa:

import { Resend } from "resend";

const resend = new Resend(process.env.RESEND_API_KEY);

export async function sendTaskAssignedEmail(
  task: Task,
  assignee: User
) {
  await resend.emails.send({
    from: "FlowTask <notifications@flowtask.app>",
    to: assignee.email,
    subject: `You've been assigned: ${task.title}`,
    react: TaskAssignedEmail({ task, assignee }),
  });
}

Probando tu API

Antes de publicar, prueba cada endpoint exhaustivamente.

Usando curl

# Listar tareas
curl -H "Authorization: Bearer ft_your_api_key" \
  http://localhost:3000/api/v1/tasks

# Crear una tarea
curl -X POST \
  -H "Authorization: Bearer ft_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"title": "Test task", "projectId": "clx..."}' \
  http://localhost:3000/api/v1/tasks

# Actualizar una tarea
curl -X PUT \
  -H "Authorization: Bearer ft_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"status": "DONE"}' \
  http://localhost:3000/api/v1/tasks/clx123

# Eliminar una tarea
curl -X DELETE \
  -H "Authorization: Bearer ft_your_api_key" \
  http://localhost:3000/api/v1/tasks/clx123

Escribiendo Tests de Integracion

Write integration tests for the FlowTask API using vitest:

1. Test each endpoint: GET list, GET single, POST create, PUT update, DELETE
2. Test authentication: requests without API key return 401
3. Test validation: missing required fields return 400
4. Test authorization: users can only access their own data
5. Test pagination: verify page/perPage/total in meta
6. Test filtering: verify status and priority filters work

Use a test database (SQLite in-memory) and seed test data before each test.

Prueba primero los caminos felices y luego los casos de error. Una API bien testeada te da confianza para iterar rapidamente.

import { describe, it, expect, beforeEach } from "vitest";

describe("GET /api/v1/tasks", () => {
  it("returns paginated tasks for authenticated user", async () => {
    const response = await fetch("/api/v1/tasks?page=1&perPage=10", {
      headers: { Authorization: `Bearer ${testApiKey}` },
    });

    expect(response.status).toBe(200);
    const json = await response.json();
    expect(json.data).toBeInstanceOf(Array);
    expect(json.meta.page).toBe(1);
    expect(json.meta.perPage).toBe(10);
  });

  it("returns 401 without API key", async () => {
    const response = await fetch("/api/v1/tasks");
    expect(response.status).toBe(401);
  });
});

Que Sigue

FlowTask ahora tiene una API publica, soporte de webhooks, procesamiento de pagos y notificaciones por email. Es una plataforma real con la que otros desarrolladores pueden integrarse.

En la proxima leccion, vamos aun mas lejos agregando funcionalidades potenciadas por IA a FlowTask --- un chatbot, resumen de texto, busqueda semantica con embeddings y generacion de contenido. Vas a aprender la diferencia entre usar IA para construir apps y construir apps que usan IA. Ambas habilidades son esenciales.