Examples

TanStack Start

Source Code
Using evlog with TanStack Start — automatic wide events, structured errors, and logging in API routes and server functions.

Practical patterns for using evlog with TanStack Start. TanStack Start uses Nitro v3 as its server layer, so evlog integrates via the evlog/nitro/v3 module.

Setup

Starting from a TanStack Start project created with npm create @tanstack/start@latest:

1. Install evlog

npm install evlog

2. Add nitro.config.ts

Create a nitro.config.ts at the project root to register the evlog module. Your vite.config.ts already has the nitro() plugin from the CLI — no changes needed there.

nitro.config.ts
import { defineConfig } from 'nitro'
import evlog from 'evlog/nitro/v3'

export default defineConfig({
  experimental: {
    asyncContext: true,
  },
  modules: [
    evlog({
      env: { service: 'my-app' },
    }),
  ],
})

Enabling asyncContext lets you access the request-scoped logger from anywhere in the call stack via useRequest().

3. Error handling middleware

TanStack Start has its own error handling layer that runs before Nitro's. To ensure throw createError() returns a proper JSON response with why, fix, and link, add the evlogErrorHandler middleware to your root route:

src/routes/__root.tsx
import { createRootRoute } from '@tanstack/react-router'
import { createMiddleware } from '@tanstack/react-start'
import { evlogErrorHandler } from 'evlog/nitro/v3'

export const Route = createRootRoute({
  server: {
    middleware: [createMiddleware().server(evlogErrorHandler)],
  },
  // ... head, shellComponent, etc.
})

That's it. evlog automatically captures every request as a wide event with method, path, status, and duration.

Wide Events

With experimental.asyncContext: true, use useRequest() from nitro/context to access the request-scoped logger and build up context progressively:

src/routes/api/hello.ts
import { createFileRoute } from '@tanstack/react-router'
import { useRequest } from 'nitro/context'
import type { RequestLogger } from 'evlog'

export const Route = createFileRoute('/api/hello')({
  server: {
    handlers: {
      GET: async () => {
        const req = useRequest()
        const log = req.context.log as RequestLogger

        log.set({ user: { id: 'user_123', plan: 'pro' } })
        log.set({ action: 'fetch_profile' })
        log.set({ cache: { hit: true, ttl: 3600 } })

        return Response.json({ ok: true })
      },
    },
  },
})

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

Terminal output
14:58:15 INFO [my-app] GET /api/hello 200 in 52ms
  ├─ cache: hit=true ttl=3600
  ├─ action: fetch_profile
  ├─ user: id=user_123 plan=pro
  └─ requestId: 4a8ff3a8-...
useRequest() is an experimental Nitro v3 feature powered by AsyncLocalStorage. It works on Node.js and Bun runtimes.

Error Handling

Use createError for structured errors with why, fix, and link fields:

src/routes/api/checkout.ts
import { createFileRoute } from '@tanstack/react-router'
import { useRequest } from 'nitro/context'
import { createError } from 'evlog'
import type { RequestLogger } from 'evlog'

export const Route = createFileRoute('/api/checkout')({
  server: {
    handlers: {
      POST: async ({ request }) => {
        const req = useRequest()
        const log = req.context.log as RequestLogger
        const body = await request.json()

        log.set({ user: { id: body.userId, plan: body.plan } })
        log.set({ cart: { items: body.items, total: body.total } })

        const result = await chargeCard(body)

        if (!result.success) {
          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',
          })
        }

        return Response.json({ success: true, orderId: result.orderId })
      },
    },
  },
})

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

Terminal output
14:58:20 ERROR [my-app] POST /api/checkout 402 in 104ms
  ├─ error: name=EvlogError message=Payment failed status=402
  ├─ cart: items=3 total=9999
  ├─ user: id=user_123 plan=pro
  └─ requestId: 880a50ac-...

Parsing Errors on the Client

Use parseError to extract the structured fields from any error response:

import { parseError } from 'evlog'

try {
  const res = await fetch('/api/checkout', {
    method: 'POST',
    body: JSON.stringify({ userId: 'user_123' }),
  })
  if (!res.ok) throw { data: await res.json(), status: res.status }
} catch (error) {
  const { message, status, why, fix, link } = parseError(error)
}

Drain & Enrichers

Since TanStack Start uses Nitro v3, configure drains and enrichers via Nitro plugins. Create a server/plugins/ directory and register hooks:

server/plugins/evlog-drain.ts
import { definePlugin } from 'nitro'
import { createAxiomDrain } from 'evlog/axiom'

export default definePlugin((nitroApp) => {
  const axiom = createAxiomDrain()

  nitroApp.hooks.hook('evlog:drain', axiom)
})
server/plugins/evlog-enrich.ts
import { definePlugin } from 'nitro'
import { createUserAgentEnricher, createRequestSizeEnricher } from 'evlog/enrichers'

export default definePlugin((nitroApp) => {
  const enrichers = [createUserAgentEnricher(), createRequestSizeEnricher()]

  nitroApp.hooks.hook('evlog:enrich', (ctx) => {
    for (const enricher of enrichers) enricher(ctx)
  })
})

Run Locally

git clone https://github.com/HugoRCD/evlog.git
cd evlog/examples/tanstack-start
bun install
bun run dev

Open http://localhost:3000 and navigate to the evlog Demo page to test the API endpoints.

Source Code

Browse the complete TanStack Start example source on GitHub.