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:
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 superjsonimport 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.