Node.js und Logging – Teil 2: Der EnvVarService
Unsere Node.js-Applikation soll je Umgebung anders konfiguriert werden. Hierfür nutzen wir Umgebungsvariablen. Damit wir innerhalb der Applikation typsicheren Zugriff haben, schreiben wir einen Service, der beim Applikationsstart sicherstellt, dass alles korrekt konfiguriert ist, und wenn nicht, sofort einen Hinweis zur Fehlerursache gibt.
Nachdem wir im Teil 1 den LogService vorbereitet haben, ist nun das Ziel, einen Service für den sicheren Zugriff auf Umgebungsvariablen bereitzustellen. Mit Sicherheit meine ich zweierlei Dinge. Zum einen, dass die Umgebungsvariablen gesetzt sind. Zum anderen, dass die Zeichenkette inhaltlich den Anforderungen unserer Applikation entspricht – z. B. die Transformation in Listen, Flags oder Zahlen. Dafür nutzen wir das NPM-Paket Zod.
Zur Zielerreichung implementieren wir einen EnvVarService, der uns Zugriff auf zwei Umgebungsvariablen liefert:
- NODE_ENV gibt uns Auskunft darüber, in welchem Modus die Node.js-Applikation läuft.
- SERVER_PORT definiert den Port, auf dem die Applikation lauschen soll.
Definition erlaubter Werte für NODE_ENV
Die Umgebungsvariable NODE_ENV kann grundsätzlich jeglichen Wert annehmen, den wir ihr zuweisen. Damit wir nicht Gefahr laufen, zwischen prod und production oder dev und development zu raten, legen wir die Werte fest. Hierfür definieren wir den erlaubten Wertebereich als Enum in der Datei types.ts im Verzeichnis src.
export enum NodeEnvironments {
DEVELOPMENT = 'development',
PRODUCTION = 'production',
TEST = 'test',
}
Erstellen des EnvVarServices
Im Ordner services erstellen wir eine neue Datei namens EnvVarService.ts. Dieser Service ist unsere zentrale Zugriffsstelle auf Werte, die wir aus den Umgebungsvariablen entnehmen. Damit wir keine lange Liste haben, gliedern wir die Werte nach Kontext. Zusätzlich stellen wir eine Hilfsfunktion bereit, um für alle Umgebungen außer Production Standardwerte festzulegen.
Warum keine Standardwerte in Production?
Eine Änderung eines Standardwerts ist der Definition nach ein Breaking Change für Production, weil sich das exakte Verhalten nicht vorhersehen lässt. Im besten Fall stürzt die Applikation ab und wir haben sofort volle Aufmerksamkeit für das Problem. Im schlimmsten Fall verhält sich die Applikation anders, und wir erfahren vom Problem hoffentlich durch Monitoring oder Logs – die Zuordnung des Fehlers zu etwas Trivialem wie der Anpassung eines Standardwerts ist noch eine ganz eigene Herausforderung. Glaubt mir das bitte: Ich habe das bereits für euch ausprobiert … mehrmals. Deshalb niemals Standardwerte nutzen, sondern immer alles explizit definieren – am besten per Infrastructure as Code.
Zurück zur Hilfsmethode, die wir wie folgt definieren:
import { z } from 'zod'
// We create a helper function to set default values only in non-production mode
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const withDevDefault = <T extends z.ZodTypeAny>(schema: T, val: any) =>
process.env['NODE_ENV'] !== NodeEnvironments.PRODUCTION ? schema.default(val) : schema
Nun gilt es, die Umgebungsvariablen einzulesen, mit dem Zod-Parser zu validieren, wenn nötig, Fehler auszugeben und die geprüften Werte typsicher verfügbar zu machen.
const parseBaseValues = () => {
// Base schema for all app-related environment variables
const schema = z.object({
NODE_ENV: withDevDefault(z.enum(NodeEnvironments), NodeEnvironment.DEVELOPMENT),
SERVER_PORT: withDevDefault(z.coerce.number().positive(), 8080),
})
const parsed = schema.safeParse(process.env)
// handle error, so app doesn't start with invalid env vars
if (!parsed.success) {
throw new Error(
`❌ Invalid environment variables in app schema: ${JSON.stringify(parsed.error, null, 2)}`,
)
}
return {
nodeEnv: parsed.data.NODE_ENV,
serverPort: parsed.data.SERVER_PORT,
}
}
// Export the parsed env vars per context: e.g. app or logging
export default {
app: parseBaseValues(),
}
Ich möchte an dieser Stelle folgende Aspekte hervorheben:
- Die Variable SERVER_PORT wird vom String-Typ in den Number-Typ umgewandelt und soll einen positiven Wert haben. Probier es mit
export SERVER_PORT=0 && npm run start:devaus. - Die Variable NODE_ENV ist ein String, erlaubt jedoch nur zwei Ausprägungen, die wir im Enum definiert haben. Benötigen wir eine weitere Umgebung, reicht es den Enum zu erweitern – z. B.
QAoder dergleichen. Probier es mitexport NODE_ENV=dev && npm run start:devaus.
Die resultierende Fehlerausgabe zeigt die genaue Fehlerquelle und mit der Zod-Fehlerausgabe haben wir eine präzise Vorstellung davon, was falsch konfiguriert ist.
Beispielhafte Nutzung des Services
Mit dem neuen Service können wir den Applikationsstart dahingehend verfeinern, dass wir uns ausgeben lassen, in welchem Kontext wir laufen und welchen Port wir nutzen.
import createApp from '@/createApp'
import LogService from '@/services/LogService'
import EnvVarService from '@/services/EnvVarService'
const PORT = EnvVarService.app.serverPort
const NODE_ENV = EnvVarService.app.nodeEnv
const app = createApp()
app.listen(PORT, () => {
LogService.debug(`Server is started`, {port: PORT, nodeEnv: NODE_ENV})
})
Fazit
Mit der Anpassung können wir unsere Applikation starten, ohne Umgebungsvariablen zu definieren. Dies hat folgende Ausgabe zur Folge:
[2026-01-03T12:01:23.659Z] debug Server is started [{"port":8080,"nodeEnv":"development"}]
Hier wird noch einmal deutlich, was ich oben erwähnt habe. Statt auf Port 4000 zu lauschen, lauschen wir auf Port 8080. Ein Breaking Change, der in der Produktivumgebung zu Problemen geführt hätte. Um auf dem vorherigen Port zu lauschen, können wir die Applikation wie folgt starten:
export SERVER_PORT=4000 && npm run start:dev
[2026-01-03T12:06:16.580Z] debug Server is started [{"port":4000,"nodeEnv":"development"}]
Damit wir zukünftig nicht über einen Exportbefehl die Umgebungsvariablen setzen müssen, werden wir im nächsten Artikel das NPM-Paket dotenv einbinden.