Skip to main content

Contrato de respuesta (CustomResponse)

La regla: ningún endpoint devuelve data cruda. Todos devuelven un CustomResponse.

Ubicación: proteus-backend/src/config/globals.ts. Espejo en el front: proteus-frontend/src/app/interfaces/api.response.ts (ApiResponse<T>).

class CustomResponse<T = object> {
success: boolean; // arranca en false
message: string;
data: T;
err: Error;
totalRows: number;

ok(data: T, totalRows?: number) // success = true
errorMessage(message: string) // success = false, data = null
error(err: Error) // success = false, data = null, err = err
hasException(): boolean
asEmpty()
}

Uso en un service:

const response = new CustomResponse();
// ...
response.ok(rows, total); // paginado: el total sin paginar va en totalRows
return response;

La consecuencia importante

Un error de negocio viaja como HTTP 200 con success: false

No como 4xx ni 5xx. Por eso el frontend necesita un errorInterceptor que convierta ese success: false en un throw + toast.

Y por eso un 401 genuino en el front sí significa sesión inválida: el backend usa 500 para errores internos y 200 + success:false para errores de negocio.

Los dos caminos de error del backend

CaminoStatus HTTPForma
Error de negocio / excepción200 (o 500 para internos){ success: false, message, data: null, err }
Error de validación (class-validator)400 real{ success: false, message: "Error de validación", data: [{ field, messages }] }

El segundo lo produce el CustomErrorHandler (src/middlewares/v1/validationErrorMiddleware.ts), y es el que ejercitan los tests de integración.

Paginación

data = la página, totalRows = el total sin paginar. El contrato de query params es offset / limit (+ sort como array JSON-stringificado); el armado del ORDER BY desde ese sort lo hace la clase Service.

Del lado del front, toda tabla es lazy y server-side:

onLazyLoad(event: TableLazyLoadEvent): void {
const offset = event.first ?? 0;
this.first.set(offset);
this.load(offset, event.rows ?? this.limit);
}

El errorInterceptor del frontend

Solo actúa sobre requests a la API. Cuando detecta success === false:

  • toastea el message y lanza un Error, así el catchError de la vista se dispara igual que con un error HTTP;
  • no toastea si la respuesta trae data, ni si el mensaje matchea /l[ií]m[ií]t|excedid|exceeded/i (los errores de límite los muestra la vista con su propio copy);
  • ante un 401 llama handleSessionExpired() (toast + redirect a login);
  • ante un 403 no desloguea: es falta de permisos.
Consecuencia práctica en una vista

No toastees el error de un request a la API — ya está toasteado. Tu catchError es solo para dejar la UI consistente (results.set([]), total.set(0)).

Sí usá el toast a mano para los éxitos ("Centro médico guardado") y para errores de validación locales.

Gotchas

ok(data, totalRows) ignora totalRows si es 0

La implementación hace if (totalRows). Con cero resultados el campo no viaja: en el front tratá res.totalRows ?? 0.

  • errorMessage() pone data = null. Y el interceptor del front no toastea si hay data — así que un error con payload no muestra toast. Es intencional para los errores de límite y de validación.
  • error(e) guarda el Error en err, y un Error serializa a {} en JSON. El front recibe {"success":false,"err":{}} sin ninguna pista. El mensaje real está en la consola del servidor. Cuando escribas un catch, completá también message para que la próxima falla se vea en la respuesta.
  • endTransaction decide commit o rollback leyendo response.success. Si te olvidás de setear el response antes del finally, rollbackea trabajo bueno en silencio.
  • El whitelist del ORDER BY descarta en silencio los campos que no mapea: si el orden "no anda", probablemente el campo no esté en el mapeo. No es un bug de la tabla.