ZeroToShip

Docs/Feature

// feature

Gestione degli errori

Gli errori in ZeroToShip hanno tre livelli, tutti già montati: il client HTTP che trasforma ogni risposta fallita in un toast rosso, l'error boundary di Next.js per le eccezioni di rendering, e la pagina 404. Non devi scrivere try/catch con alert() da nessuna parte.

apiClient, il pezzo centrale

libs/api.ts esporta un'istanza axios con baseURL: “/api” e un interceptor di risposta. Usalo sempre al posto di fetch per chiamare le tue route in app/api/.

libs/api.ts
ts
const apiClient = axios.create({
  baseURL: "/api",
});

apiClient.interceptors.response.use(
  function (response) {
    return response.data;
  },
  ...
);

Prima conseguenza pratica: in caso di successo l'interceptor ritorna direttamente response.data. Quindi non scrivi res.data.url, scrivi:

components/ButtonCheckout.tsx
tsx
const { url }: { url: string } = await apiClient.post(
  "/stripe/create-checkout",
  { priceId, mode: "payment", successUrl, cancelUrl }
);

Cosa fa l'interceptor in errore

Questo è il comportamento reale, riga per riga:

libs/api.ts
ts
function (error) {
  let message = "";

  if (error.response?.status === 401) {
    // User not auth, ask to re login
    toast.error("Please login");
    redirect(config.auth.loginUrl);
  } else if (error.response?.status === 403) {
    // User not authorized, must subscribe/purchase/pick a plan
    message = "Pick a plan to use this feature";
  } else {
    message =
      error?.response?.data?.error || error.message || error.toString();
  }

  error.message =
    typeof message === "string" ? message : JSON.stringify(message);

  console.error(error.message);

  if (error.message) {
    toast.error(error.message);
  } else {
    toast.error("something went wrong...");
  }
  return Promise.reject(error);
}
  • 401 — toast “Please login” e redirect(config.auth.loginUrl), cioè /signin. La sessione è scaduta o non c'è: si rifà il login e basta.
  • 403 — messaggio “Pick a plan to use this feature”. È lo status da usare nelle tue route quando l'utente è loggato ma non ha pagato (vedi Pagamenti).
  • Tutto il resto — prende error.response.data.error, cioè il campo error del JSON che ha ritornato la tua API. Se manca, ripiega su error.message di axios (utile per timeout e problemi di rete).

In tutti i casi: console.error per te, toast.error per l'utente, e Promise.reject — l'errore continua a propagarsi, quindi il tuo catch nel componente viene comunque eseguito.

Nota di traduzione: quei messaggi nel repo sono in inglese (“Please login”, “Pick a plan to use this feature”, “something went wrong...”). Se la tua app è in italiano, cambiali in libs/api.ts — sono hardcoded lì, non passano da nessun sistema di traduzioni.

Il pattern nei componenti

Siccome il toast lo mostra già l'interceptor, nel componente ti serve solo gestire lo stato di caricamento. È esattamente quello che fanno ButtonCheckout, ButtonAccount e ButtonLead:

components/MioBottone.tsx
tsx
const [isLoading, setIsLoading] = useState(false);

const handleClick = async () => {
  setIsLoading(true);
  try {
    await apiClient.post("/qualcosa", { foo: "bar" });
  } catch (e) {
    // il toast è già partito dall'interceptor: qui logghi e basta
    console.error(e);
  } finally {
    setIsLoading(false);
  }
};

Lato API: ritorna sempre un campo error

Perché l'interceptor abbia qualcosa di leggibile da mostrare, le tue route handler devono rispondere con un JSON che contiene la chiave error e lo status giusto:

app/api/esempio/route.ts
ts
import { NextResponse } from "next/server";
import { createClient } from "@/libs/supabase/server";

export async function POST(req: Request) {
  const supabase = await createClient();
  const { data: { user } } = await supabase.auth.getUser();

  if (!user) {
    return NextResponse.json({ error: "Non sei autenticato" }, { status: 401 });
  }

  try {
    const body = await req.json();
    if (!body.priceId) {
      return NextResponse.json({ error: "priceId mancante" }, { status: 400 });
    }
    return NextResponse.json({ ok: true });
  } catch (e) {
    console.error(e);
    return NextResponse.json({ error: "Errore del server" }, { status: 500 });
  }
}

Restituisci 403 (non 401) quando l'utente è loggato ma senza accesso al piano: cambia completamente il messaggio che vede.

Il Toaster

react-hot-toast è tra le dipendenze e il <Toaster /> è già montato in components/LayoutClient.tsx con durata 3 secondi:

components/LayoutClient.tsx
tsx
<Toaster
  toastOptions={{
    duration: 3000,
  }}
/>

Quindi da qualsiasi Client Component puoi fare toast.success(“Fatto”) o toast.error(“Ops”) senza altro setup — è quello che fa la pagina /signin dopo aver inviato il magic link.

error.tsx — l'error boundary

app/error.tsx è già nel repo. È la convenzione App Router: deve essere un Client Component, riceve error e reset, e Next.js lo monta automaticamente quando un componente sotto di lui lancia durante il rendering. Quello nel repo mostra error.message, un bottone Refresh che chiama reset(), un link alla home e un <ButtonSupport />.

app/dashboard/error.tsx
tsx
"use client";

export default function Error({
  error,
  reset,
}: {
  error: Error;
  reset: () => void;
}) {
  return (
    <div className="p-8 text-center">
      <p>Qualcosa è andato storto nella dashboard.</p>
      <p className="text-red-500">{error?.message}</p>
      <button className="btn-brutal" onClick={reset}>Riprova</button>
    </div>
  );
}

Puoi metterne uno per segmento: un error.tsx dentro app/dashboard/ cattura solo gli errori di quel ramo e lascia in piedi header e navigazione. Due cose da ricordare: un error.tsx non cattura gli errori del layout che gli sta sopra (per quello serve un global-error.tsx in root, che nel repo non c'è — aggiungilo tu se ti serve), e in produzione Next.js oscura i messaggi degli errori server per non farti perdere dati sensibili.

not-found.tsx — la 404

app/not-found.tsx è già nel repo, con illustrazione, link alla home e <ButtonSupport />. Viene renderizzata per gli URL che non corrispondono a nessuna route, e la puoi invocare a mano quando una risorsa non esiste:

app/blog/[slug]/page.tsx
tsx
import { notFound } from "next/navigation";

const post = await getPost(params.slug);
if (!post) notFound();

Come per error.tsx, puoi aggiungere un not-found.tsx dentro un segmento per avere una 404 contestuale (“questo articolo non esiste”) invece di quella generica.