This episode tidies up the configuration side: environment variables for the base URL, API keys, and development mode, a tRPC configuration structure shared between client and server, and the application of a conditional loggerLink between development and production.

Good code never stores configuration hardcoded. Base URLs, API keys, and development mode should live in environment variables, and tRPC is no exception. Episode 7 discusses how to manage that configuration cleanly: setting up environment variables, arranging shared configuration between client and server, and applying loggerLink conditionally.
After this episode, your project will be ready to move between environments — local, staging, and production — without changing a single line of application code.
Create a .env.local file in the project root (make sure it is in your gitignore list):
NODE_ENV=development
PUBLIC_BASE_URL=http://localhost:3000
INTERNAL_API_KEY=rahasiadilarang-di-commitThree types of variables are commonly used:
NODE_ENV marks development or production mode.In Next.js and Vite, variables accessed by the client must be prefixed with NEXT_PUBLIC_ or VITE_. Variables without a prefix are only available on the server:
NEXT_PUBLIC_BASE_URL=https://api.contoh.com
DATABASE_URL=postgres://localhost:5432/belajar
SECRET_SIGNING_KEY=sangat-rahasiaThe client can read NEXT_PUBLIC_BASE_URL, but DATABASE_URL and secrets stay safe on the server. Write down every required variable in .env.example so team members know what to fill in — never put secret values in the example file.
Ideally, configuration is accessed through a single module shared between server and client. Create a config.ts file:
const publicBaseUrl = process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000";
export const config = {
isDev: process.env.NODE_ENV === "development",
publicBaseUrl,
trpcUrl: `${publicBaseUrl}/api/trpc`,
wsUrl: process.env.NEXT_PUBLIC_WS_URL ?? "ws://localhost:3000/trpc",
};
export const serverConfig = {
internalApiKey: process.env.INTERNAL_API_KEY ?? "",
databaseUrl: process.env.DATABASE_URL ?? "",
};config is safe to access anywhere, including the client, because it only holds public values.serverConfig is only used on the server — it holds secrets that must not leak into the client bundle.With this pattern, changing the base URL only requires editing one file. The client and server use the same values, so there is no chance of unsynced URLs.
The tRPC client module reads from config:
import { createTRPCReact } from "@trpc/react-query";
import { httpBatchLink } from "@trpc/client";
import { config } from "@/lib/config";
export const trpc = createTRPCReact<AppRouter>();
export const trpcClient = trpc.createClient({
links: [
httpBatchLink({
url: config.trpcUrl,
}),
],
});config.trpcUrl derives the endpoint from NEXT_PUBLIC_BASE_URL, so changing the domain only requires changing an environment variable.
loggerLink is useful in development but noisy in production. Enable it conditionally with enabled:
import { loggerLink } from "@trpc/client";
import { config } from "@/lib/config";
export const trpcClient = trpc.createClient({
links: [
loggerLink({
enabled: () => config.isDev,
}),
httpBatchLink({
url: config.trpcUrl,
}),
],
});loggerLink({ enabled: () => config.isDev }) only logs requests when NODE_ENV is development. In production, this link outputs no logs at all — saving bandwidth and avoiding information leaks through logs.
On the server side, logging can be added through middleware — exactly the logger pattern from episode 6. By combining config.isDev, middleware can log full details in development and only errors in production:
const logger = t.middleware(async ({ path, type, next }) => {
const mulai = Date.now();
const hasil = await next();
if (config.isDev || !hasil.ok) {
console.log(`${type} ${path} selesai ${Date.now() - mulai}ms`);
}
return hasil;
});config.isDev || !hasil.ok makes production log only failures, while development logs everything.
Warning
Never access serverConfig or secrets inside code that is imported by the client. Variables without the NEXT_PUBLIC_ prefix are not available in the browser — accessing them from the client produces undefined and can leak through bundling if imported directly.
Episode 7 tidies up configuration: environment variables become the single source of truth, the shared configuration module keeps client and server in sync, and loggerLink is enabled conditionally so development stays comfortable without polluting production.
Key takeaways:
NEXT_PUBLIC_ prefix only for values safe for the client to access..env.example as a list of required variables.config and secret serverConfig.loggerLink({ enabled: () => isDev }) for conditional logging.In the next episode, episode 8, we will discuss state management & data fetching patterns — query, mutation, invalidate, and optimistic update patterns, modern @tanstack/react-query integration, as well as caching, refetch, and stale data handling techniques.