Introduction to Logging in TypeScript Applications
\n\nLogging is the process of recording events that occur during program execution. For TypeScript applications, especially those running in a Node.js environment, properly configured logging is not just good practice, but a critically important tool for debugging, monitoring, and error analysis.
\n\nMany beginner developers limit themselves to using console.log(). However, in a production environment, this approach is ineffective: it does not allow you to flexibly manage severity levels, filter messages, or save them to files. In this article, we will look at how to create a professional logging system in TypeScript from scratch.
Why You Shouldn't Use console.log in Production?
\n\nLet's look at a typical example:
\n\n// Bad approach — using console.log directly\nfunction processOrder(orderId: string): void {\n console.log(`Processing order ${orderId}`);\n try {\n // business logic\n } catch (error) {\n console.error(`Error processing order: ${error}`);\n }\n}\n\n\nProblems with this approach:
\n- \n
- Lack of logging levels: you cannot separate debug messages from critical errors. \n
- No formatting: difficult to read large amounts of data. \n
- Cannot redirect: all messages go only to stdout/stderr. \n
- Performance issues: synchronous console writing can slow down the application. \n
Best Practices for Logging in TypeScript
\n\n1. Choosing a Logging Library
\n\nFor TypeScript, it is recommended to use one of two popular libraries:
\n- \n
- Winston — a powerful and flexible logger with support for transports (file, console, databases). \n
- Pino — an ultra-fast logger, ideal for high-load applications. \n
Let's install Winston:
\nnpm install winston\nnpm install @types/winston --save-dev\n\n\n2. Creating a Configurable Logger
\n\nLet's create a logger.ts module:
import winston from 'winston';\n\nconst logger = winston.createLogger({\n level: process.env.LOG_LEVEL || 'info',\n format: winston.format.combine(\n winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),\n winston.format.errors({ stack: true }),\n winston.format.json()\n ),\n transports: [\n new winston.transports.Console({\n format: winston.format.combine(\n winston.format.colorize(),\n winston.format.simple()\n )\n }),\n new winston.transports.File({ \n filename: 'logs/error.log', \n level: 'error' \n }),\n new winston.transports.File({ \n filename: 'logs/combined.log' \n })\n ]\n});\n\nexport default logger;\n\n\nNow you can use the logger in any module:
\nimport logger from './logger';\n\nfunction getUser(id: string) {\n logger.info(`Fetching user with ID: ${id}`);\n try {\n // user retrieval logic\n logger.debug('User data retrieved successfully');\n } catch (error) {\n logger.error('Failed to fetch user', { userId: id, error });\n }\n}\n\n\n3. Using Logging Levels
\n\nStandard levels (from least to most important):
\n- \n
- debug — detailed debugging information (development only). \n
- info — standard informational messages. \n
- warn — warnings about potential issues. \n
- error — errors requiring attention. \n
Configure the logging level via environment variables:
\n// In .env file\nLOG_LEVEL=debug # for development\nLOG_LEVEL=warn # for production\n\n\n4. Structured Logging
\n\nPass additional