ixiclinicDocs
DesarrolladoresGuías

Escribir una server action

El puente entre el admin y la API mediante el api-client, headers de auth desde cookies y refresh automático.

En el admin (y en console/portal) toda comunicación con la API pasa por server actions. No hay fetch del lado del cliente. Una action es una función "use server" en apps/admin/src/actions/<dominio>.ts que llama al api-client.

El api-client

apps/admin/src/lib/api-client.ts expone cinco funciones genéricas:

FunciónUso
apiGet<T>(path)Lecturas. Usa cache: "no-store".
apiPost<T>(path, body?)Crear / acciones.
apiPatch<T>(path, body?)Actualizaciones parciales.
apiPut<T>(path, body?)Reemplazos.
apiDelete<T>(path)Borrar (maneja 204 No Content).

Lo que hacen por ti, para que tu action no lo repita:

  • Headers de auth: getAuthHeaders() lee accessToken de las cookies (next/headers) y arma el Authorization: Bearer ….
  • Refresh automático en 401: si la API responde 401, tryRefreshToken() usa el refreshToken (conservando tenantId/branchId del switcher de sucursal) para renovar el accessToken, reescribe la cookie httpOnly y reintenta la request una vez. Si no hay refresh válido, redirige a /login.
  • Errores estructurados: el payload de error de la API (statusCode, code, message, y campos extra como occupiedBy/idleMin) se preserva en el Error, para que tu action pueda ramificar (p. ej. el "tomar caja ocupada" del módulo cash).

Nota: apiPatch y apiDelete omiten Content-Type cuando no hay body, porque Fastify rechaza un body JSON vacío (FST_ERR_CTP_EMPTY_JSON_BODY). No fuerces el header.

Anatomía de un archivo de actions

"use server";

import { apiGet, apiPost } from "@/lib/api-client";
import { revalidatePath } from "next/cache";
  • La directiva "use server" va al inicio del archivo.
  • Importa solo las funciones del api-client que necesites.
  • Tras una escritura, llama revalidatePath(...) para refrescar la pantalla que muestra esos datos.

Ejemplo de lectura

"use server";

import { apiGet } from "@/lib/api-client";

export async function getWidgets(filters?: { status?: string; limit?: number }) {
  const params = new URLSearchParams();
  if (filters?.status) params.set("status", filters.status);
  if (filters?.limit) params.set("limit", String(filters.limit));
  const qs = params.toString();
  return apiGet<Widget[]>(`/api/widgets${qs ? `?${qs}` : ""}`);
}

export async function getWidget(id: string) {
  return apiGet<Widget>(`/api/widgets/${id}`);
}

Ejemplo de escritura

"use server";

import { apiPost } from "@/lib/api-client";
import { revalidatePath } from "next/cache";

export async function createWidget(data: { name: string; color?: string }) {
  const widget = await apiPost<Widget>("/api/widgets", data);
  revalidatePath("/widgets");
  return widget;
}

Ramificar sobre un error estructurado

Cuando la API responde un 409 con código (como hace cash), captura y devuelve un objeto que la UI pueda interpretar, en vez de dejar que reviente:

export type CreateResult =
  | { success: true; widget: Widget }
  | { error: string; conflict?: true };

export async function createWidgetSafe(data: { name: string }): Promise<CreateResult> {
  try {
    const widget = await apiPost<Widget>("/api/widgets", data);
    revalidatePath("/widgets");
    return { success: true, widget };
  } catch (e: unknown) {
    const err = e as { statusCode?: number; code?: string; message?: string };
    if (err.statusCode === 409) {
      return { error: err.message ?? "Conflicto", conflict: true };
    }
    return { error: e instanceof Error ? e.message : "Error al crear" };
  }
}

El mismo patrón en console y portal

  • Console (apps/console): mismo patrón, pero su api-client (apps/console/src/lib/api-client.ts) usa cookies con prefijo saas- (saas-accessToken, saas-refreshToken) y pega a endpoints /api/saas/*. Las actions viven en apps/console/src/actions/.
  • Portal (apps/portal): su propio api-client (apps/portal/src/lib/api-client.ts), con flujos de auth de paciente y de empresa, y el tenant resuelto vía apps/portal/src/lib/tenant.ts. Actions en apps/portal/src/actions/.

Checklist

  1. Archivo apps/admin/src/actions/<dominio>.ts con "use server" arriba.
  2. Importa solo las funciones del api-client que uses.
  3. Lecturas con apiGet, escrituras con apiPost/apiPatch/apiPut/apiDelete.
  4. revalidatePath(...) después de cada escritura.
  5. Si la API devuelve errores con código, ramifica sobre statusCode/code.

Anterior: Añadir un módulo a la API · Siguiente: Añadir una pantalla al admin.

On this page