---
title: "Quick Start"
description: "Get up and running with evlog in minutes. Learn the log API, createLogger for wide events, useLogger for requests, and structured errors."
canonical_url: "https://www.evlog.dev/start/quick-start"
---
# Quick Start

> Get up and running with evlog in minutes. Learn the log API, createLogger for wide events, useLogger for requests, and structured errors.

This guide covers the core APIs you'll use most often with evlog. Prefer to learn by running a complete server? Follow [Structured Logging in Node.js](https://www.evlog.dev/use-cases/structured-logging-nodejs), then return here for the API overview.

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

In Nuxt, evlog **auto-imports** `useLogger`, `log`, and `parseError`. `createError` is imported from `evlog`: the auto-imported one is Nuxt's own, and it drops `why`, `fix`, and `link`.

</callout>

<prompt :actions="["copy","cursor","claude"]" description="Get evlog running in 2 minutes" icon="i-lucide-zap">

Get evlog running in my project in under 2 minutes.

- Install evlog: pnpm add evlog
- Create evlog.config.ts with export default defineEvlog({ service: 'my-app' })
- Detect my framework and wire up the matching integration so it loads evlog.config.ts
- Add a single useLogger(event) call in a route handler with log.set({ ... })
- Throw one createError({ message, status, why, fix }) for an invalid input case
- Trigger the route locally and confirm a single wide event prints to the terminal

Docs: [https://www.evlog.dev/start/quick-start](https://www.evlog.dev/start/quick-start)
Frameworks: [https://www.evlog.dev/integrate/frameworks/overview](https://www.evlog.dev/integrate/frameworks/overview)

</prompt>

## Configure once in evlog.config.ts

Every option lives in one file at the root of your app. `npx evlog init` writes it for you:

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

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

Nuxt, Nitro and TanStack Start load it on their own; other frameworks import it in one line, shown in each [framework guide](https://www.evlog.dev/integrate/frameworks/overview). See [Configuration](https://www.evlog.dev/reference/configuration) for every option.

## log (Simple Logging)

The simplest way to use evlog. Fire-and-forget structured logs, anywhere in your code:

<code-group>

```typescript [Server]
import { log } from 'evlog'

log.info('auth', 'User logged in')
log.error({ action: 'payment', error: 'card_declined' })
log.warn('cache', 'Cache miss')
```

```bash [Output]
10:23:45.612 [auth] User logged in
10:23:45.613 ERROR [my-app] action=payment error=card_declined
10:23:45.614 [cache] Cache miss
```

</code-group>

Two call styles:

- **Tagged**: `log.info('tag', 'message')` for quick, readable console output
- **Structured**: `log.info({ key: value })` for rich events that flow through the drain pipeline

Coming from pino or consola, the default import is the same `log` API with no setup line: `import logger from 'evlog'`, then `logger.info('auth', 'User logged in')` prints right away. The object form takes the event alone, so pino's `logger.info(obj, msg)` becomes `logger.info(obj)`. The no-setup path has a price in production: without `initLogger()`, events carry no redaction and no drain, and the first emitted event logs a one-time warning pointing at `initLogger({ drain })`.

<callout color="neutral" icon="i-lucide-arrow-right">

See the full [Simple Logging](https://www.evlog.dev/learn/simple-logging) guide for all patterns and drain integration.

</callout>

## createLogger (Wide Events)

When you need to **accumulate context** across multiple steps of an operation, whether a script, background job, queue worker, or workflow, use `createLogger`:

<code-group>

```typescript [scripts/sync-job.ts]
import { initLogger, createLogger, toLoggerConfig } from 'evlog'
import config from '../evlog.config'

initLogger(toLoggerConfig(config))

const log = createLogger({ jobId: job.id, queue: 'emails' })

log.set({ batch: { size: 50 } })
log.set({ batch: { processed: 50 } })
log.emit()
```

```bash [Output (Pretty)]
10:23:45.612 INFO [my-app] in 1204ms
  ├─ jobId: job_abc123
  ├─ queue: emails
  └─ batch: size=50 processed=50
```

</code-group>

`createLogger()` accepts any initial context as a plain object. It returns a logger with `set`, `error`, `info`, `warn`, `emit`, and `getContext`.

For HTTP request contexts specifically, use `createRequestLogger()` which pre-populates `method`, `path`, and `requestId`:

```typescript [src/worker.ts]
import { createRequestLogger } from 'evlog'

const log = createRequestLogger({ method: 'POST', path: '/api/checkout' })
```

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

With `createLogger` and `createRequestLogger`, you must call `log.emit()` manually. In framework integrations, this happens automatically.

</callout>

## useLogger (Retrieve the Request Logger)

When using a framework integration (Nuxt, Hono, Express, etc.), the middleware automatically creates a wide event logger on request start and emits it on response end. `useLogger(event)` retrieves that logger from the request context:

<framework-tabs>

```typescript [Nuxt]server/api/checkout.post.ts
import { useLogger } from 'evlog'

export default defineEventHandler(async (event) => {
  const log = useLogger(event)

  log.set({ user: { id: 1, plan: 'pro' } })
  log.set({ cart: { items: 3, total: 9999 } })

  const order = await processCheckout()
  log.set({ orderId: order.id })

  return { success: true, orderId: order.id }
})
```

```typescript [Nitro]routes/api/checkout.post.ts
import { defineHandler } from 'nitro/h3'
import { useLogger } from 'evlog/nitro/v3'

export default defineHandler(async (event) => {
  const log = useLogger(event)

  log.set({ user: { id: 1, plan: 'pro' } })
  log.set({ cart: { items: 3, total: 9999 } })

  const order = await processCheckout()
  log.set({ orderId: order.id })

  return { success: true, orderId: order.id }
})
```

```typescript [Next.js]app/api/checkout/route.ts
import { withEvlog, useLogger } from '@/lib/evlog'

export const POST = withEvlog(async () => {
  const log = useLogger()

  log.set({ user: { id: 1, plan: 'pro' } })
  log.set({ cart: { items: 3, total: 9999 } })

  const order = await processCheckout()
  log.set({ orderId: order.id })

  return Response.json({ success: true, orderId: order.id })
})
```

```typescript [Hono]src/index.ts
app.post('/api/checkout', async (c) => {
  const log = c.get('log')

  log.set({ user: { id: 1, plan: 'pro' } })
  log.set({ cart: { items: 3, total: 9999 } })

  const order = await processCheckout()
  log.set({ orderId: order.id })

  return c.json({ success: true, orderId: order.id })
})
```

```typescript [Express]src/index.ts
app.post('/api/checkout', async (req, res) => {
  const log = req.log

  log.set({ user: { id: 1, plan: 'pro' } })
  log.set({ cart: { items: 3, total: 9999 } })

  const order = await processCheckout()
  log.set({ orderId: order.id })

  return res.json({ success: true, orderId: order.id })
})
```

</framework-tabs>

```bash [Output (Pretty)]
10:23:45.612 INFO [my-app] POST /api/checkout 200 in 234ms
  ├─ user: id=1 plan=pro
  ├─ cart: items=3 total=9999
  └─ orderId: ord_abc123
```

<callout color="success" icon="i-lucide-check">

`useLogger` doesn't create a logger, the framework middleware already did that. It just retrieves it from the event context so you can add data with `set()`.

</callout>

### When to use what

<table>
<thead>
  <tr>
    <th>
      Use <code>
        log
      </code>
    </th>
    
    <th>
      Use <code>
        createLogger()
      </code>
      
       / <code>
        createRequestLogger()
      </code>
    </th>
    
    <th>
      Use <code>
        useLogger(event)
      </code>
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Quick one-off events
    </td>
    
    <td>
      Scripts, jobs, workers, queues, HTTP without a framework
    </td>
    
    <td>
      API routes with a framework integration
    </td>
  </tr>
  
  <tr>
    <td>
      No context accumulation needed
    </td>
    
    <td>
      Accumulate context over an operation
    </td>
    
    <td>
      Retrieve the request-scoped logger
    </td>
  </tr>
  
  <tr>
    <td>
      Client-side logging
    </td>
    
    <td>
      Wide events (one log per operation)
    </td>
    
    <td>
      Access the auto-managed wide event
    </td>
  </tr>
</tbody>
</table>

### Service Identification

In multi-service architectures, differentiate which service a log belongs to using either route-based configuration or explicit service names.

#### Route-Based Configuration

Map route patterns to service names in `evlog.config.ts`:

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

export default defineEvlog({
  service: 'default-service',
  routes: {
    '/api/auth/**': { service: 'auth-service' },
    '/api/payment/**': { service: 'payment-service' },
    '/api/booking/**': { service: 'booking-service' },
  },
})
```

Nuxt, Nitro and TanStack Start pick it up; other frameworks pass it with `toMiddlewareOptions(config)`. Route mapping needs a framework integration: `initLogger` does not take `routes`.

Logs from routes matching these patterns will automatically include the configured service name:

```bash [Output]
21:57:10.442 INFO [auth-service] POST /api/auth/login 200 in 1ms
  ├─ requestId: 88ced16a-bef2-4483-86cb-2b4fb677ea52
  ├─ user: id=user_123 email=demo@example.com
  └─ action: login
```

#### Explicit Service Parameter

Override the service name for specific routes using the second parameter of `useLogger`:

```typescript [server/api/legacy/process.post.ts]
import { useLogger } from 'evlog'

export default defineEventHandler((event) => {
  const log = useLogger(event, 'legacy-service')

  log.set({ action: 'process_legacy_request' })

  return { success: true }
})
```

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

**Priority order:** Explicit `useLogger` parameter > Route configuration > `service` in `evlog.config.ts` > Auto-detected from environment

</callout>

## createError (Structured Errors)

Use `createError()` to throw errors with actionable context:

<code-group>

```typescript [services/checkout.ts]
import { createError } from 'evlog'

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',
})
```

```json [Response]
{
  "statusCode": 402,
  "message": "Payment failed",
  "data": {
    "why": "Card declined by issuer",
    "fix": "Try a different payment method",
    "link": "https://docs.example.com/payments/declined"
  }
}
```

</code-group>

### Error Fields

<table>
<thead>
  <tr>
    <th>
      Field
    </th>
    
    <th>
      Required
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        message
      </code>
    </td>
    
    <td>
      Yes
    </td>
    
    <td>
      What happened (user-facing)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        status
      </code>
    </td>
    
    <td>
      No
    </td>
    
    <td>
      HTTP status code (default: 500)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        why
      </code>
    </td>
    
    <td>
      No
    </td>
    
    <td>
      Technical reason (for debugging)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        fix
      </code>
    </td>
    
    <td>
      No
    </td>
    
    <td>
      Actionable solution
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        link
      </code>
    </td>
    
    <td>
      No
    </td>
    
    <td>
      Documentation URL for more info
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        cause
      </code>
    </td>
    
    <td>
      No
    </td>
    
    <td>
      Original error (if wrapping)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        internal
      </code>
    </td>
    
    <td>
      No
    </td>
    
    <td>
      Backend-only fields for logs and wide events — never included in HTTP JSON or <code>
        parseError()
      </code>
    </td>
  </tr>
</tbody>
</table>

### Frontend Integration

Use `parseError()` to extract all error fields on the client:

```typescript [composables/useCheckout.ts]
import { parseError } from 'evlog'

export async function checkout(cart: Cart) {
  try {
    await $fetch('/api/checkout', { method: 'POST', body: cart })
  } catch (err) {
    const error = parseError(err)

    toast.add({
      title: error.message,
      description: error.why,
      color: 'error',
      actions: error.link
        ? [{ label: 'Learn more', onClick: () => window.open(error.link) }]
        : undefined,
    })

    if (error.fix) {
      console.info(`Fix: ${error.fix}`)
    }
  }
}
```

## log (Client-Side)

The same `log` API works on the client side, outputting to the browser console:

<code-group>

```vue [components/CheckoutButton.vue]
<script setup lang="ts">
async function handleCheckout() {
  log.info('checkout', 'User initiated checkout')

  try {
    await $fetch('/api/checkout', { method: 'POST' })
    log.info({ action: 'checkout', status: 'success' })
  } catch (err) {
    log.error({ action: 'checkout', error: 'failed' })
  }
}
</script>
```

```typescript [composables/useAnalytics.ts]
export function useAnalytics() {
  function trackEvent(event: string, data?: Record<string, unknown>) {
    log.info('analytics', `Event: ${event}`)
    if (data) {
      log.debug({ event, ...data })
    }
  }

  return { trackEvent }
}
```

</code-group>

<callout color="neutral" icon="i-lucide-arrow-right">

See [Client Logging](https://www.evlog.dev/use-cases/client-logging) for drain configuration, identity context, and browser setup.

</callout>

## How much of this do you need on day one?

Two calls. Everything else on this page is available when you want it, and nothing breaks while you do not.

`log.info()` behaves like `console.log`, so the first commit is a find-and-replace that changes no behaviour. From there, the two that pay for themselves are `log.set()`, which adds a field to the event already in flight, and `createError()`, which attaches a `why` and a `fix` to a failure instead of a bare stack.

Migrate one handler at a time. A route still calling `console.log` next to a route using `log.set()` is a normal intermediate state, not a broken one: they write to the same place and neither knows about the other. Pick the handler you last debugged in production and start there, or let [`evlog map`](https://www.evlog.dev/cli/map) rank them for you.

Typed fields, catalogs, sampling and drains are all opt-in, and each one is worth reaching for the day you feel its absence rather than the day you install.

## Next Steps

- [`evlog map`](https://www.evlog.dev/cli/map): See what you just unlocked. Score coverage and find entry points that are still dark (`npx evlog map`)
- [Logging Overview](https://www.evlog.dev/learn/overview): Understand all three logging modes
- [Wide Events](https://www.evlog.dev/learn/wide-events): Learn how to design effective wide events
- [Typed Fields](https://www.evlog.dev/learn/typed-fields): Add compile-time type safety to your wide events
- [Structured Errors](https://www.evlog.dev/learn/structured-errors): Master error handling with evlog
- [Best Practices](https://www.evlog.dev/reference/best-practices): Security guidelines and production tips

---

- [Logging Overview](https://www.evlog.dev/learn/overview)
- [Structured Errors](https://www.evlog.dev/learn/structured-errors)


## Sitemap

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