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.

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.

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

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.

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.