KwadMarket Docs
Best Practices

API Layer

tRPC vs plain REST fetch — what to adopt and what to skip

Decision: no tRPC migration

Migrating to tRPC is a large refactor not warranted for the current scope — @marketplace/types already fills the type-sharing gap adequately, and lib/api/server.ts (RSC cache() + serverFetch) is the correct pattern and a strength. The targeted improvements below close the most impactful gaps instead.

The cal.diy pattern

tRPC with request batching and superjson serializer:

apps/web/app/_trpc/trpc-client.ts
export const trpcClient = trpc.createClient({
  links: [
    loggerLink({ ... }),
    splitLink({
      condition: (op) => !!op.context.skipBatch,
      true: httpLink({ ... }),
      false: httpBatchLink({ ... }),   // multiple queries → 1 HTTP request
    }),
  ],
  transformer: superjson,             // Date, Map, Set survive serialization
});

Benefits: end-to-end type safety without a shared types package, automatic query batching, built-in Date/Map/Set serialization, typed errors.

Targeted improvements for the REST layer

1. superjson for date round-trips (optional)

pnpm --filter @marketplace/web add superjson
apps/web/lib/api/http.ts
import superjson from "superjson";

async function request<T>(url: string, options?: RequestInit): Promise<T> {
  const res = await fetch(url, options);
  const json = await res.json();
  return superjson.deserialize<T>(json);  // dates come back as Date objects
}

Requires the backend to also serialize with superjson — only adopt if manual .toISOString() conversions become a real pain point.

2. Batch independent queries manually

// Pattern for pages with multiple independent data requirements
const [products, categories] = await Promise.all([
  fetchProducts(params),
  fetchCategories(),
]);

Already possible in RSC pages — should be standard practice.

3. Normalize error shapes (the highest-value item)

Add a global ExceptionFilter in NestJS that always returns:

{ "statusCode": 422, "message": "Validation failed", "errors": [{ "field": "email", "message": "Required" }] }

Then update apps/web/lib/api/http.ts to parse errors[]:

class ApiError extends Error {
  constructor(
    public readonly statusCode: number,
    message: string,
    public readonly errors?: { field?: string; message: string }[]
  ) {
    super(message);
  }
}

This lets mutation hooks set react-hook-form field errors:

onError(err) {
  if (err instanceof ApiError && err.errors) {
    err.errors.forEach(({ field, message }) => {
      if (field) form.setError(field as FieldPath<FormData>, { message });
    });
  }
}

See DTO validation for the backend half of this contract.

Future option — incremental tRPC adoption

If tRPC is ever adopted, start with one new feature domain (e.g. notifications or admin analytics) rather than migrating existing endpoints — tRPC and plain REST can coexist in the same Next.js app. A cheaper alternative: generate the client from Swagger once Operations §2 lands.

On this page