---
title: "NestJS"
description: "`EvlogModule.forRoot()` registers the middleware, the exception filter and `useLogger()` across a NestJS app, with async config where you need it."
canonical_url: "https://www.evlog.dev/integrate/frameworks/nestjs"
---
# NestJS

> `EvlogModule.forRoot()` registers the middleware, the exception filter and `useLogger()` across a NestJS app, with async config where you need it.

The `evlog/nestjs` module provides `EvlogModule.forRoot()` which registers a global middleware, creating a request-scoped logger accessible via `useLogger()` or `req.log`, emitting a wide event when the response completes.

<prompt :actions="["copy","cursor","claude"]" description="Set up evlog in my NestJS app" icon="i-simple-icons-nestjs">

Set up evlog in my NestJS app.

- Install evlog: pnpm add evlog
- Create evlog.config.ts with export default defineEvlog({ service: 'my-api' })
- Import EvlogModule from 'evlog/nestjs' and add EvlogModule.forRoot(toMiddlewareOptions(config)) to AppModule imports
- Call initLogger(toLoggerConfig(config)) in main.ts
- The global middleware auto-creates a request-scoped logger for every request
- Use useLogger() in any controller or service to access the logger
- Use log.set() to accumulate context, throw createError() for structured errors
- Optionally pass drain, enrich, and keep callbacks to forRoot()

Docs: [https://www.evlog.dev/integrate/frameworks/nestjs](https://www.evlog.dev/integrate/frameworks/nestjs)
Adapters: [https://www.evlog.dev/integrate/adapters/overview](https://www.evlog.dev/integrate/adapters/overview)

</prompt>

## Quick Start

### 1. Install

<code-group>

```bash [pnpm]
pnpm add evlog @nestjs/common @nestjs/core @nestjs/platform-express
```

```bash [bun]
bun add evlog @nestjs/common @nestjs/core @nestjs/platform-express
```

```bash [yarn]
yarn add evlog @nestjs/common @nestjs/core @nestjs/platform-express
```

```bash [npm]
npm install evlog @nestjs/common @nestjs/core @nestjs/platform-express
```

</code-group>

### 2. Create evlog.config.ts

```typescript [evlog.config.ts]
import { defineEvlog } from 'evlog'

export default defineEvlog({
  service: 'my-api',
})
```

Every option lives in this file: sampling, drain, `enrich`, route filters. `npx evlog init` writes it, and the wiring below, for you. See [Configuration](https://www.evlog.dev/reference/configuration).

### 3. Register the module

```typescript [src/app.module.ts]
import { Module } from '@nestjs/common'
import { toMiddlewareOptions } from 'evlog'
import { EvlogModule } from 'evlog/nestjs'
import config from '../evlog.config'

@Module({
  imports: [
    EvlogModule.forRoot(toMiddlewareOptions(config)),
  ],
})
export class AppModule {}
```

### 4. Bootstrap with evlog

```typescript [src/main.ts]
import 'reflect-metadata'
import { NestFactory } from '@nestjs/core'
import { initLogger, toLoggerConfig } from 'evlog'
import { AppModule } from './app.module'
import config from '../evlog.config'

initLogger(toLoggerConfig(config))

const app = await NestFactory.create(AppModule)
await app.listen(3000)
```

`EvlogModule.forRoot()` registers as a global module, so the middleware is automatically applied to all routes.

## Fastify Platform

The module works the same on `@nestjs/platform-fastify`. Install it in place of `@nestjs/platform-express` and pass the adapter to `NestFactory.create()`:

```typescript [src/main.ts]
import 'reflect-metadata'
import { NestFactory } from '@nestjs/core'
import { FastifyAdapter } from '@nestjs/platform-fastify'
import type { NestFastifyApplication } from '@nestjs/platform-fastify'
import { initLogger } from 'evlog'
import { AppModule } from './app.module'

initLogger({
  env: { service: 'my-api' },
})

const app = await NestFactory.create<NestFastifyApplication>(AppModule, new FastifyAdapter())
await app.listen(3000)
```

Nest runs middleware on the raw Node request, so on Fastify the module also registers a `preHandler` hook that sets the evlog logger on `FastifyRequest.log`, in place of Fastify's own request logger. Guards, interceptors, pipes, and controllers all get the evlog logger from `request.log`. Fastify `onRequest` hooks run before it and still see Fastify's logger.

Do not register the `evlog/fastify` plugin on the same app: each one emits its own wide event, so every request is logged twice.

## Wide Events

Build up context progressively through your controllers and services. One request = one wide event:

```typescript [src/users.controller.ts]
import { Controller, Get, Param } from '@nestjs/common'
import { useLogger } from 'evlog/nestjs'

@Controller('users')
export class UsersController {
  @Get(':id')
  async findOne(@Param('id') id: string) {
    const log = useLogger()

    log.set({ user: { id } })

    const user = await db.findUser(id)
    log.set({ user: { name: user.name, plan: user.plan } })

    const orders = await db.findOrders(id)
    log.set({ orders: { count: orders.length, totalRevenue: sum(orders) } })

    return { user, orders }
  }
}
```

All fields are merged into a single wide event emitted when the request completes:

```bash [Terminal output]
14:58:15 INFO [my-api] GET /users/usr_123 200 in 12ms
  ├─ orders: count=2 totalRevenue=6298
  ├─ user: id=usr_123 name=Alice plan=pro
  └─ requestId: 4a8ff3a8-...
```

## useLogger()

Use `useLogger()` to access the request-scoped logger from anywhere in the call stack without injecting the request object through your service layer:

```typescript [src/users.service.ts]
import { useLogger } from 'evlog/nestjs'

export class UsersService {
  async findUser(id: string) {
    const log = useLogger()
    log.set({ user: { id } })

    const user = await db.findUser(id)
    log.set({ user: { name: user.name, plan: user.plan } })

    return user
  }
}
```

```typescript [src/users.controller.ts]
@Controller('users')
export class UsersController {
  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.usersService.findUser(id)
  }
}
```

Both `req.log` and `useLogger()` return the same logger instance. `useLogger()` uses `AsyncLocalStorage` to propagate the logger across async boundaries.

## Background work (`log.fork`)

Use `req.log.fork(label, fn)` (or the logger from `useLogger()` in the same request) for child wide events. See [After emit](https://www.evlog.dev/learn/wide-events#after-emit-sealing-and-background-work) in Wide events.

```typescript [src/orders.controller.ts]
import { useLogger } from 'evlog/nestjs'

@Post()
create(@Req() req: Express.Request) {
  req.log.fork!('enqueue', async () => {
    const log = useLogger()
    log.set({ queued: true })
  })
  return { ok: true }
}
```

## Error Handling

Use `createError` for structured errors with `why`, `fix`, and `link` fields. Create a NestJS exception filter to log and format errors:

```typescript [src/evlog-exception.filter.ts]
import { Catch } from '@nestjs/common'
import type { ExceptionFilter, ArgumentsHost } from '@nestjs/common'
import { parseError } from 'evlog'
import { useLogger } from 'evlog/nestjs'

@Catch()
export class EvlogExceptionFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const response = host.switchToHttp().getResponse()
    const error = exception instanceof Error ? exception : new Error(String(exception))

    try {
      useLogger().error(error)
    } catch {
      // Outside an evlog request scope — log is unavailable
    }

    const parsed = parseError(error)
    response.status(parsed.status).json({
      message: parsed.message,
      why: parsed.why,
      fix: parsed.fix,
      link: parsed.link,
    })
  }
}
```

Apply it to your controllers:

```typescript [src/checkout.controller.ts]
import { Controller, Get, UseFilters } from '@nestjs/common'
import { createError } from 'evlog'
import { EvlogExceptionFilter } from './evlog-exception.filter'

@Controller()
@UseFilters(new EvlogExceptionFilter())
export class CheckoutController {
  @Get('checkout')
  checkout() {
    throw createError({
      message: 'Payment failed',
      status: 402,
      why: 'Card declined by issuer',
      fix: 'Try a different payment method',
      link: 'https://docs.example.com/payments/declined',
    })
  }
}
```

The error is captured and logged with both the custom context and structured error fields:

```bash [Terminal output]
14:58:20 ERROR [my-api] GET /checkout 402 in 3ms
  ├─ error: name=EvlogError message=Payment failed status=402
  └─ requestId: 880a50ac-...
```

## Configuration

Every option goes in `evlog.config.ts`. See the [Configuration reference](https://www.evlog.dev/reference/configuration) for the full list.

## Drain & Enrichers

Set `drain` and `enrich` in `evlog.config.ts` so they reach the app through `toMiddlewareOptions(config)`. You can also pass them inline in `EvlogModule.forRoot()`; an inline key replaces the one from the file instead of merging with it:

```typescript [src/app.module.ts]
import { Module } from '@nestjs/common'
import { EvlogModule } from 'evlog/nestjs'
import { createAxiomDrain } from 'evlog/axiom'
import { createUserAgentEnricher } from 'evlog/enrichers'

const userAgent = createUserAgentEnricher()

@Module({
  imports: [
    EvlogModule.forRoot({
      drain: createAxiomDrain(),
      enrich: (ctx) => {
        userAgent(ctx)
        ctx.event.region = process.env.FLY_REGION
      },
    }),
  ],
})
export class AppModule {}
```

### Async Configuration

Use `forRootAsync()` when options depend on other providers (e.g. `ConfigService`):

```typescript [src/app.module.ts]
import { Module } from '@nestjs/common'
import { ConfigModule, ConfigService } from '@nestjs/config'
import { EvlogModule } from 'evlog/nestjs'
import { createAxiomDrain } from 'evlog/axiom'

@Module({
  imports: [
    ConfigModule.forRoot(),
    EvlogModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        drain: createAxiomDrain({ apiKey: config.get('AXIOM_API_KEY') }),
      }),
    }),
  ],
})
export class AppModule {}
```

### Pipeline (Batching & Retry)

For production, wrap your adapter with `createDrainPipeline` to batch events and retry on failure:

```typescript [src/app.module.ts]
import type { DrainContext } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'
import { createDrainPipeline } from 'evlog/pipeline'

const pipeline = createDrainPipeline<DrainContext>({
  batch: { size: 50, intervalMs: 5000 },
  retry: { maxAttempts: 3 },
})
const drain = pipeline(createAxiomDrain())

EvlogModule.forRoot({ drain })
```

<callout color="info" icon="i-lucide-info">

Call `drain.flush()` on server shutdown to ensure all buffered events are sent. See the [Pipeline docs](https://www.evlog.dev/extend/drain-pipeline) for all options.

</callout>

## Tail Sampling

Use `keep` to force-retain specific events regardless of head sampling:

```typescript [src/app.module.ts]
EvlogModule.forRoot({
  drain: createAxiomDrain(),
  keep: (ctx) => {
    if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
  },
})
```

## Route Filtering

Control which routes are logged with `include` and `exclude` patterns:

```typescript [src/app.module.ts]
EvlogModule.forRoot({
  include: ['/api/**'],
  exclude: ['/_internal/**', '/health'],
  routes: {
    '/api/auth/**': { service: 'auth-service' },
    '/api/payment/**': { service: 'payment-service' },
  },
})
```

## Run Locally

```bash [Terminal]
git clone https://github.com/evloghq/evlog.git
cd evlog
pnpm install
pnpm example nestjs
```

Open [http://localhost:3000](http://localhost:3000) to explore the interactive test UI.

<card-group>
<card icon="i-simple-icons-github" title="Source Code" to="https://github.com/evloghq/evlog/tree/main/examples/nestjs">

Browse the complete NestJS example source on GitHub.

</card>
</card-group>

## Next Steps

Deepen your **NestJS** integration:

- [Wide Events](https://www.evlog.dev/learn/wide-events): Design comprehensive events with context layering
- [Adapters](https://www.evlog.dev/integrate/adapters/overview): Send logs to Axiom, Sentry, PostHog, and more
- [Sampling](https://www.evlog.dev/learn/sampling): Control log volume with head and tail sampling
- [Structured Errors](https://www.evlog.dev/learn/structured-errors): Throw errors with `why`, `fix`, and `link` fields

---

- [Source Code](https://github.com/evloghq/evlog/tree/main/examples/nestjs)


## Sitemap

See the full [sitemap](https://www.evlog.dev/sitemap.md) for all pages.
