Logging in TypeScript Applications: Best Practices in Practice
Logging is a fundamental part of any production application. It allows you not only to track code execution and find errors, but also to conduct security audits, analyze performance, and debug complex scenarios. In TypeScript, with its powerful type system, the approach to logging becomes even more flexible and reliable. In this article, we will break down the best logging practices using concrete examples.
1. Choosing a Library and Configuring the Logger
The first thing to start with is choosing a suitable library. Among the popular solutions, winston and pino stand out. Both support TypeScript out of the box, but have different priorities: winston offers flexibility and rich functionality, while pino focuses on maximum performance.
Let's look at an example of configuring winston with typing:
import winston from 'winston';
const logger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', format: winston.format.combine( winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.errors({ stack: true }), winston.format.json() ), defaultMeta: { service: 'user-service' }, transports: [ new winston.transports.Console({ format: winston.format.combine( winston.format.colorize(), winston.format.simple() ), }), new winston.transports.File({ filename: 'logs/error.log', level: 'error' }), new winston.transports.File({ filename: 'logs/combined.log' }), ],});
export default logger;For pino, the configuration will be more concise, but no less effective:
import pino from 'pino';
const logger = pino({ level: process.env.LOG_LEVEL || 'info', transport: { target: 'pino-pretty', options: { colorize: true, translateTime: 'SYS:standard', }, },});
export default logger;Tip: Always externalize the log level configuration to environment variables (LOG_LEVEL). In production, info or warn is typically used, while in development, debug is used.
2. Structured Logging and Context
One of the main mistakes is logging simple strings. Instead, use structured objects. They allow you to easily filter and aggregate logs in systems like ELK, Grafana, or Datadog.
Example of bad logging:
logger.info('User logged in successfully');Example of good logging with context:
logger.info({ message: 'User logged in', userId: user.id, email: user.email, loginMethod: 'oauth', ip: request.ip, duration: Date.now() - startTime,});For convenience, you can create a typed wrapper:
interface LogContext { requestId?: string; userId?: string; action?: string; [key: string]: unknown;}
function logWithContext(message: string, context: LogContext = {}): void { logger.info({ message, ...context });}
// UsagelogWithContext('Order processed', { orderId: '12345', userId: 'user_678', total: 99.99 });This approach ensures that every log contains key information for analysis.
3. Logging Errors and Exceptions
Error handling is the most important part of logging. Always log the full error stack and the context in which it occurred. Do not use console.error — it does not provide flexibility for rotation and formatting.
The correct pattern with TypeScript:
import logger from './logger';
async function fetchUserData(userId: string): Promise<UserData> { try { const response = await api.get(`/users/${userId}`); ret