Implementing Observability in NestJS via Prometheus and Grafana Orchestration

The operational stability of a production-grade backend system is often measured not by the absence of bugs, but by the speed of detection and the visibility of system health. In modern microservices architecture, the absence of a robust monitoring layer is a precursor to catastrophic failure. Real-world scenarios have demonstrated that production applications can remain in a state of downtime for six hours or more—only being recovered after users initiate support requests and developers are forced to manually reboot services. Such incidents result in direct financial loss, erosion of customer trust, and significant developer fatigue. To prevent these "blind" outages, a developer must implement a structured observability pipeline consisting of three primary actors: the NestJS web application (the data producer), Prometheus (the metrics collection and storage engine), and Grafana (the visualization and alerting interface). By integrating these tools, developers can transform unstructured, ephemeral application logs into meaningful, actionable, and persistent time-series dashboards.

The Critical Role of Observability in Backend Development

Monitoring is not merely a luxury for high-traffic systems; it is a foundational requirement for any production-ready software. The primary objective of implementing a metrics pipeline is to move away from reactive troubleshooting and toward proactive system management. Without a dedicated metrics exporter, a NestJS application exists as a black box, where developers can only infer system health through external symptoms rather than internal telemetry.

The implementation of this stack provides several layers of utility:

  1. Real-time visibility into request latency and throughput.
  2. Error rate tracking to detect regressions immediately after deployment.
  3. Resource utilization monitoring to inform scaling decisions.
  4. Automated alerting capabilities to notify engineering teams before a failure becomes a customer-facing outage.

While tools like Apollo Studio offer advanced metrics and features for GraphQL-based architectures, they frequently come with the drawback of vendor lock-in and the requirement for expensive paid plans. Implementing a self-hosted Prometheus and Grafana stack allows for complete control over the data lifecycle and avoids the costs associated with proprietary monitoring platforms.

Architecture of the Monitoring Pipeline

The orchestration of a monitoring ecosystem requires a coordinated flow of data between distinct software components. This setup is typically achieved using Docker to ensure environmental consistency across development and production environments.

The pipeline operates through a specific sequence of data movement:

  • The NestJS application acts as the source, exposing a specific HTTP endpoint (such as /metrics or /app-metrics) where the current state of the system is published.
  • Prometheus serves as the scraper, periodically performing HTTP requests to the NestJS endpoint to pull the latest metric values.
  • Grafana acts as the consumer, querying Prometheus via its query language to render visual representations of the scraped data.

This architecture ensures that the application remains decoupled from the visualization layer, allowing for independent scaling of the monitoring infrastructure.

Technical Requirements and Environment Setup

Before initiating the configuration, the local development environment must be equipped with specific runtime engines and container orchestration tools.

The following prerequisites are mandatory:

  • Node.js: The runtime environment required to execute the NestJS application and manage dependencies.
  • Docker: The containerization engine used to run Prometheus, Grafana, and the NestJS app in isolated, reproducible environments.
  • Package Manager: Either npm or yarn for dependency installation and script execution.

To initialize a new project, the NestJS CLI can be utilized to generate a scaffolded application:

bash npx @nestjs/cli new metr101

During this process, the user must select npm as the preferred package manager. Once the project structure is created, the application can be initialized and run using the following commands:

bash npm i npm start

Upon successful execution, the application will be accessible at http://localhost:3000/, providing a "Hello World!" confirmation of a functional baseline.

Implementing the Prometheus Metrics Module in NestJS

The core of the telemetry implementation involves integrating the @willsoto/nestjs-prometheus library along with the prom-client library. This integration allows the NestJS framework to interface directly with the Prometheus registry.

To begin the integration, the following dependency installation is required:

bash npm install @willsoto/nestjs-prometheus prom-client

Or, if utilizing the Yarn package manager:

bash yarn add @willsoto/nestjs-prometheus prom-client

Configuring the Prometheus Module

The PrometheusModule must be registered within the AppModule to define the endpoint where metrics will be exposed. This registration is critical because it dictates the URI that Prometheus will scrape.

typescript PrometheusModule.register({ path: '/app-metrics', })

By setting the path to /app-metrics, the developer explicitly defines the target for the Prometheus scraper. This configuration also allows for the definition of custom metric providers, such as Counters and Gauges, which track specific business or technical logic.

Defining Custom Metric Providers

Metrics are categorized based on how they represent data. A Counter is a cumulative metric that only increases (e.g., total number of requests), while a Gauge represents a value that can go up or down (e.g., current memory usage or request duration).

In the AppModule providers array, the following configurations can be implemented:

typescript makeCounterProvider({ name: 'count', help: 'metric_help', labelNames: ['method', 'origin'] as string[], }) makeGaugeProvider({ name: 'gauge', help: 'metric_help', })

The structural components of these providers include:

  • name: The unique identifier for the metric within the Prometheus ecosystem.
  • help: A descriptive string that explains the purpose of the metric to other engineers.
  • labelNames: An array of strings used to add dimensions to the metric, such as HTTP methods or origin URLs, allowing for granular filtering in Grafana.

Advanced GraphQL Monitoring Integration

For applications utilizing Apollo GraphQL, a specialized plugin approach is necessary to capture fine-grained execution data. This involves creating a PromModule that includes the GraphQLPromienteMetricsPlugin and various pre-defined counters.

The module configuration should be structured as follows:

```typescript
import { Module } from '@nestjs/common';
import { PrometheusModule } from '@willsoto/nestjs-prometheus';
import {
GraphQLPrometheusMetricsPlugin,
validationStartedCounter,
parsedCounter,
resolvedCounter,
executionStartedCounter,
errorsCounter,
respondedCounter,
}
from './prometheus.plugin';

@Module({
imports: [PrometheusModule.register()],
providers: [
GraphQLPrometheusMetricsPlugin,
validationStartedCounter,
parsedCounter,
resolvedCounter,
executionStartedCounter,
errorsCounter,
respondedCounter,
],
exports: [GraphQLPrometheusMetricsPlugin],
})
export class PromModule {}
```

To ensure these metrics are actually captured during the GraphQL lifecycle, the plugin must be injected into the GraphQLGatewayModule configuration:

typescript @Module({ imports: [ ...PromModule, GraphQLGatewayModule.forRootAsync({ imports: [...PromTRomModule], useFactory: async (...graphQLPrometheusMetrics: GraphQLPrometheusMetricsPlugin) => { return { server: { plugins: [graphQLPrometheusMetrics], }, }; }, }), ], }) export class AppModule {}

This configuration enables the tracking of specific GraphQL stages, such as validationStarted, parsed, resolved, and errors, providing a deep look into the request lifecycle.

Implementing Middleware for Request Latency and Error Tracking

Beyond standard counters, developers can implement custom middleware to capture the duration of HTTP requests and track error rates based on response status codes. This is achieved by intercepting the response lifecycle.

The following logic demonstrates how to calculate request duration and increment error counters:

```typescript
const startTime = Date.now();

res.on('finish', () => {
const endTime = Date.now();
const duration = endTime - startTime;

this.customDurationGauge
.labels(req.method, req.originalUrl, (duration / 1000).toString())
.set(duration);

this.customErrorsCounter.labels(req.method, req.originalUrl, res.statusCode.toString()).inc();
});

next();
```

This implementation uses the finish event of the response object to ensure that the calculation occurs only after the response has been fully sent to the client. The use of .labels() allows for multidimensional analysis, enabling a developer to see exactly which endpoint is experiencing high latency.

Containerization and Infrastructure Orchestration

To manage the complexity of running three separate services (NestJS, Prometheus, and Grafana), Docker Compose is utilized. This ensures that the entire monitoring stack can be spun up with a single command and that the services can communicate over a shared Docker network.

The Dockerfile for NestJS

The NestJS application requires a multi-stage Dockerfile to optimize the production image size. The first stage handles the build process, while the second stage contains only the necessary artifacts for execution.

```dockerfile
FROM node:22-alpine AS builder
WORKDIR /app
COPY package.json ./
COPY nest-cli.json ./
COPY tsconfig
.json ./
COPY src/ ./src
RUN npm ci && npm run build

FROM node:22-alpine
WORKDIR /app
COPY --from=builder /app/nodemodules ./nodemodules
COPY --from=builder /app/dist ./dist
CMD [ "node", "dist/main.js" ]
```

The Docker Compose Configuration

The docker-compose.yml file defines the interconnected services and their respective port mappings and volumes.

```yaml
services:
metr101-app:
build: .
containername: metr101-app
ports:
- "3000:3000"
depends
on:
- metr101-prometheus

metr101-prometheus:
image: prom/prometheus:v2.54.1
container_name: metr101-prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml

metr101-grafana:
image: grafana/grafana:11.2.2
container_name: metr101-grafana
ports:
- "3030:3000"
```

Key components of this orchestration include:

  • metr101-app: The primary application, exposed on port 3000, which depends on the Prometheus service.
  • metr101-prometheus: The metrics engine, running on port 9090, which uses a volume mount to ingest a local prometheus.yml configuration file.
  • metr101-grafana: The visualization engine, accessible on port 3030, which acts as the frontend for the data.

To deploy this infrastructure, simply run:

bash docker-compose up

Data Visualization and Dashboard Configuration in Grafana

Once the containers are operational, the final step is to configure Grafana to consume the data from Prometheus.

Initial Configuration and Data Sources

Upon the first launch, users should log in to Grafana using the default credentials:

  • Username: admin
  • Password: admin

The first critical task is to configure the Prometheus Data Source. This involves pointing Grafana to the Prometheus service URL (e.htmlhttp://metr101-prometheus:9090). Without this configuration, Grafana has no way of knowing where to fetch the metrics.

Creating and Customizing Dashboards

To avoid building dashboards from scratch, developers can leverage existing community templates. A common practice is to search for the "Node Exporter Full" dashboard ID within the Grafana dashboard settings to quickly populate the interface with standard system metrics.

Once the data source is linked, custom visualization can be performed for application-specific metrics. For example, a Counter exported from NestJS can be visualized as a time-series graph showing the rate of change in request volume over time. Developers can further customize these dashboards to include:

  • Heatmaps for latency distribution.
  • Stat panels for current error counts.
  • Gauges for real-time memory or CPU usage.

Comparative Analysis of Implementation Approaches

The following table summarizes the different components and their responsibilities within the observability ecosystem.

Component Role Primary Technology Key Configuration Requirement
Data Producer Exposes application metrics NestJS PrometheusModule.register()
Metrics Collector Scrapes and stores time-series data Prometheus prometheus.yml scrape config
Data Visualizer Renders metrics into dashboards Grafana Data Source connection to Prometheus
Orchestrator Manages service lifecycles Docker Compose docker-compose.yml service definitions

Final Technical Evaluation

The implementation of a NestJS-Grafana-Prometheus stack represents a transition from reactive development to a disciplined, data-driven engineering culture. By utilizing @willsoto/nestjs-prometheus, developers can inject telemetry directly into the application's dependency injection container, making the monitoring logic a first-class citizen of the codebase. The use of Docker ensures that this complex multi-service architecture is portable and easy to replicate across staging and production environments.

Ultimately, the value of this setup lies in its ability to provide granular, multidimensional data through labels. When a developer can filter a dashboard by method, origin, or status_code, they are no longer just looking at "errors"—they are looking at exactly which API route is failing and for which specific users. While the initial setup requires rigorous attention to detail in both the NestJS module configuration and the Docker Compose orchestration, the long-term benefit is a resilient system capable of maintaining high availability and minimizing the devastating impacts of unmonitored downtime.

Sources

  1. nestjs-metrics GitHub Repository
  2. Apollo GraphQL and NestJS Monitoring
  3. Setup Dashboard with Grafana and NestJS
  4. Monitoring with NestJS, Prometheus, and Grafana

Related Posts