Logging in TypeScript Applications: Best Practices and Tools

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: Why Logging is Important in TypeScript

Logging is not just about writing events to a file. In modern TypeScript applications, properly configured logging is the foundation for debugging, monitoring, and performance analysis. Without quality logs, a developer spends hours reproducing errors, and the operations team cannot quickly identify bottlenecks.

TypeScript, as a superset of JavaScript, provides strict typing, which allows for creating more predictable and secure logging systems. However, using plain console.log() in production is an anti-pattern that leads to information noise and loss of important data.

In this article, we will explore best practices for logging in TypeScript: from choosing a library to structuring logs and working with context.



Choosing a Logging Library

In the TypeScript/Node.js ecosystem, there are several popular libraries. Let's consider three main options:

  • Winston — the most popular library with flexible transport configuration (output to console, file, database). Supports logging levels and formatting.
  • Pino — a modern library focused on performance. Ideal for high-load systems.
  • Bunyan — a less popular but stable option. All loggers output data in JSON format.

For a typical TypeScript application, we recommend Winston or Pino. Example of configuring Winston with TypeScript:

import winston from 'winston';

const logger = winston.createLogger({ level: 'info', format: winston.format.json(), transports: [ new winston.transports.Console(), new winston.transports.File({ filename: 'error.log', level: 'error' }), ],});

logger.info('Server started', { port: 3000 });


Integration with TypeScript: Types and Interfaces

To get the most out of typing, create a wrapper for the logger using interfaces:

interface LoggerOptions {  level?: string;  service?: string;}

interface ILogger { info(message: string, meta?: unknown): void; error(message: string, error?: Error): void; warn(message: string, meta?: unknown): void;}

This will allow you to strictly control the types of data being passed and avoid errors at the compilation stage.



Logging Levels and Their Proper Use

Standard logging levels (from most critical to least): error, warn, info, debug, trace. It is important to adhere to a consistent semantics:

  • error — only for unrecoverable errors that require developer attention.
  • warn — for potentially problematic situations that do not disrupt application operation.
  • info — for key events: server startup, request processing, successful authorization.
  • debug — for debugging during development. Do not enable this level in production.

Example of dynamically changing the level depending on the environment:

const logLevel = process.env.NODE_ENV === 'production' ? 'warn' : 'debug';const logger = winston.createLogger({ level: logLevel });


Structured Logs and Context

Instead of string messages, use structured logs in JSON format. This makes it easy to parse logs using the ELK stack (Elasticsearch, Logstash, Kibana) or other systems.

Be sure to add contextual information: request ID, username, service name, timestamp. Example using Pino:

import pino from 'pino';

const logger = pino({ level: 'info', mixin() { return { service: 'my-api' }; },});

// Adding c

Blogs

Book Recommendations