Frameworks

TanStack Start

Source Code
Automatic wide events, structured errors, and logging in TanStack Start API routes and server functions.

TanStack Start uses Nitro v3 as its server layer, so evlog integrates via the evlog/nitro/v3 module. The same plugin-based hooks system applies.

TanStack Router vs TanStack Start — TanStack Router is a client-side router and doesn't need server-side logging. This page covers TanStack Start, the full-stack framework. If you're using TanStack Router in SPA mode, see Client Logging instead.
Prompt
Set up evlog in my TanStack Start app.

- Install evlog: pnpm add evlog
- Create nitro.config.ts with evlog/nitro/v3 module and experimental.asyncContext enabled
- Configure env.service with your app name
- Add evlogErrorHandler middleware to the root route for structured error responses
- Access the logger via useRequest().context.log in route handlers
- Use log.set() to accumulate context, throw createError() for structured errors

Docs: https://www.evlog.dev/frameworks/tanstack-start
Adapters: https://www.evlog.dev/adapters

Quick Start

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

1. Install

bun add 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, so no changes are 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.

Using Vite? TanStack Start is Vite-based. The evlog/vite plugin strips log.debug() from production builds and injects source locations — add it to your vite.config.ts alongside the TanStack Start plugin.

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)
}

Configuration

See the Configuration reference for all available options (initLogger, middleware options, sampling, silent mode, etc.).

Route Filtering

Control which routes are logged with include and exclude in the module options:

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' },
      include: ['/api/**'],
      exclude: ['/_internal/**', '/health'],
      routes: {
        '/api/auth/**': { service: 'auth-service' },
        '/api/payment/**': { service: 'payment-service' },
      },
    }),
  ],
})

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)
  })
})
See the Adapters and Enrichers docs for all available drain adapters and enrichers.

Pipeline (Batching & Retry)

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

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

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

  nitroApp.hooks.hook('evlog:drain', drain)
})
Call drain.flush() on server shutdown to ensure all buffered events are sent. See the Pipeline docs for all options.

Tail Sampling

Use the evlog:emit:keep hook to force-retain specific events regardless of head sampling:

server/plugins/evlog-keep.ts
import { definePlugin } from 'nitro'

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:emit:keep', (ctx) => {
    if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
    if (ctx.status && ctx.status >= 500) ctx.shouldKeep = true
  })
})

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.