---
name: fetch_json
kind: function
lang: ts
domain: infra
version: "1.0.0"
purity: impure
signature: "async function fetchJSON<T>(path: string, init?: RequestInit, baseUrl?: string): Promise<T>"
description: "Wrapper de fetch que parsea JSON, lanza HTTPError en errores HTTP y retorna undefined en 204. Exporta la clase HTTPError con el status code de la respuesta fallida."
tags: [http, fetch, json, rest, api, error, httperror, infra]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
  - name: path
    desc: "Path relativo a baseUrl (ej: /api/users). Se concatena directamente: ${baseUrl ?? ''}${path}"
  - name: init
    desc: "RequestInit de fetch (method, body, headers, etc.). headers se mergea con el default Content-Type: application/json. credentials se puede sobreescribir (default: include)"
  - name: baseUrl
    desc: "URL base sin slash final (ej: https://api.example.com). Opcional, default string vacio"
output: "Promise<T> con el JSON parseado. Lanza HTTPError si !res.ok. Retorna undefined as T si status 204."
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/infra/fetch_json.ts"
---

## Ejemplo

```typescript
import { fetchJSON, HTTPError } from "@fn_library/../infra/fetch_json";

// GET con base URL
const users = await fetchJSON<User[]>("/users", undefined, "https://api.example.com");

// POST con body
const card = await fetchJSON<Card>(
  "/cards",
  { method: "POST", body: JSON.stringify({ title: "Nueva tarea" }) },
  "/api",
);

// Capturar HTTPError
try {
  await fetchJSON("/protected");
} catch (err) {
  if (err instanceof HTTPError) {
    console.error(`HTTP ${err.status}: ${err.message}`);
  }
}
```

## Comportamiento detallado

- **URL**: `${baseUrl ?? ""}${path}` — si baseUrl es undefined o vacío, path se usa tal cual.
- **Headers**: `{ "Content-Type": "application/json" }` como base, mergeados con `init?.headers`. Los headers de init tienen precedencia.
- **credentials**: `"include"` por defecto (envía cookies). Se puede sobreescribir pasando `credentials: "omit"` en init.
- **Error HTTP** (`!res.ok`): intenta `res.json()` para leer el cuerpo del error. Si el parse falla (body vacío, no-JSON), usa `{ Message: res.statusText }`. Construye `HTTPError(status, err.Message ?? err.message ?? res.statusText)`.
- **204 No Content**: retorna `undefined as T` sin intentar parsear el body (que está vacío).
- **Errores de red**: no se capturan — el rechazo de la Promise nativa de `fetch` se propaga tal cual.

## Notas

Extraído de `apps/kanban/frontend/src/api.ts` (líneas 14–32) y generalizado añadiendo el parámetro `baseUrl` opcional para reutilización fuera del contexto kanban.

`HTTPError` se exporta para que el consumidor pueda hacer `instanceof HTTPError` en sus catch.
