Learning Node.js - Logging, Environment Config, and Runtime Settings
Episode 12 of 23

Learning Node.js - Logging, Environment Config, and Runtime Settings

This episode tidies up application runtime: loading configuration from environment variables and the .env file, distinguishing NODE_ENV, structured logging with pino, and understanding log levels for production. All of these are requirements for a deployable application.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

An application that only runs on its developer's laptop isn't really an application yet. Real applications run in many environments — local, staging, production — each with different configuration: port, database connection, secret keys. This configuration must not be hardcoded.

Episode 12 covers three pillars of runtime configuration: environment variables and the .env file, distinguishing NODE_ENV per environment, and structured logging with pino. After this episode, your application is ready for its journey toward production.

Environment Variables and the .env File

Storing Configuration Outside the Code

An environment variable is a name-value pair injected into a process. Node.js exposes them through process.env. Secret values like JWT_SECRET from episode 11 and DATABASE_URL live here — not in the code.

The easiest way to manage them during development is the .env file:

Load the .env file
node --env-file=.env app.js

The flag node --env-file=.env app.js makes Node.js read the .env file and export it into process.env — with no extra packages. Add .env to .gitignore so secrets never enter the repository.

Reading Configuration in Code

JSRead environment variables
const port = Number(process.env.PORT ?? 3000);
const nama = process.env.APP_NAME ?? "belajar-nodejs";
 
console.log(nama, port);

The pattern process.env.PORT ?? 3000 uses a default when the variable isn't set. The Number(...) conversion matters because all process.env values are strings. The nullish operator ?? only replaces null or undefined, so an empty string is still respected.

NODE_ENV and Runtime Settings

Why NODE_ENV Matters

NODE_ENV is a convention that tells the application its current environment: development, test, or production. Many frameworks use its value to enable or disable features:

Run with NODE_ENV
NODE_ENV=production node app.js
NODE_ENV=test node --test

NODE_ENV=production node app.js makes the application run in production mode — Express disables the stack trace details that leak in error responses. In development, the default value is development when not explicitly set.

Adjusting Behavior by Environment

JSBehavior based on environment
if (process.env.NODE_ENV === "production") {
  app.set("trust proxy", 1);
}

Use NODE_ENV for behavioral differences, not for storing secrets — secrets stay in their own process.env variables. The example above only enables trust proxy in production, because forged proxy headers are dangerous when directly exposed to the internet.

Structured Logging with pino

Why console.log Isn't Enough

console.log emits text that's hard for machines to parse. In production, you need structured logging — every log line is JSON that can be filtered, collected, and queried. pino is the most popular and very fast choice:

Install pino
npm install pino

npm install pino adds a logger with JSON support and log levels. pino is very fast because it uses optimized serialization — important for servers serving thousands of requests.

Writing Structured Logs

JSStructured logging with pino
import pino from "pino";
 
const logger = pino({ level: process.env.LOG_LEVEL ?? "info" });
 
logger.info("Server mulai");
logger.info({ userId: 42 }, "Pengguna masuk");
logger.error({ err: new Error("koneksi gagal") }, "Database tidak merespons");

pino({ level: process.env.LOG_LEVEL ?? "info" }) creates a logger with the default level info. Notice the difference in forms: logger.info({ userId: 42 }, "Pesan") attaches structured context, and logger.error({ err: ... }, "...") includes the full error object with a stack trace — far more useful for debugging than plain text.

Logging Levels and Best Practices

The Five Standard Levels

pino follows the standard log levels, from quietest to most detailed: fatal, error, warn, info, debug, trace. The principle:

  • error and fatal: things that need immediate attention.
  • warn: abnormal but not fatal conditions.
  • info: normal events like the server starting.
  • debug: development details, usually turned off in production.

Set the level via LOG_LEVEL — production is usually info or warn, development is often debug. Too much logging drowns the important signals.

One Logger for Everything

Don't create several loggers with different configurations. Create a single logger module and import it across the application, so the level and format stay consistent. In episode 22, these JSON logs will be streamed directly into observability systems like Loki or OpenSearch.

Closing

Here's what to take away:

  • Store configuration and secrets in process.env, not in code.
  • node --env-file=.env loads a .env file without extra packages.
  • NODE_ENV distinguishes development, test, and production.
  • Structured logging produces query-ready JSON.
  • pino replaces console.log with fast serialization.
  • Log levels go from fatal to trace; tune LOG_LEVEL per environment.

In the next episode, episode 13, we'll discuss relational vs NoSQL databases in Node.js — SQL characteristics and ACID, the document model of NoSQL, choosing factors based on data and scenarios, and how both are used from Node.js code. This opens the data phase of this series.

Learning Node.js - Logging, Environment Config, and Runtime Settings | Learn Node.js