---
title: "TanStack Start"
description: "Automatic wide events, structured errors, and logging in TanStack Start API routes and server functions."
canonical_url: "https://www.evlog.dev/integrate/frameworks/tanstack-start"
---
# TanStack Start

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

TanStack Start uses [Nitro v3](https://www.evlog.dev/integrate/frameworks/nitro) as its server layer, so evlog integrates via the `evlog/nitro/v3` module. The same plugin-based hooks system applies.

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

**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](https://www.evlog.dev/use-cases/client-logging) instead.

</callout>

<prompt :actions="["copy","cursor","claude"]" description="Set up evlog in my TanStack Start app" icon="i-custom-tanstack">

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
- Create evlog.config.ts at the app root with export default defineEvlog({ service: 'my-app' }); the evlog/nitro/v3 module loads it, so call evlog() with no options
- 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/integrate/frameworks/tanstack-start](https://www.evlog.dev/integrate/frameworks/tanstack-start)
Adapters: [https://www.evlog.dev/integrate/adapters/overview](https://www.evlog.dev/integrate/adapters/overview)

</prompt>

## Quick Start

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

### 1. Install

<code-group>

```bash [pnpm]
pnpm add evlog
```

```bash [bun]
bun add evlog
```

```bash [yarn]
yarn add evlog
```

```bash [npm]
npm install evlog
```

</code-group>

### 2. Create evlog.config.ts

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

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

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

### 3. 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.

```typescript [nitro.config.ts]
import { defineConfig } from 'nitro'
import evlog from 'evlog/nitro/v3'

export default defineConfig({
  experimental: {
    asyncContext: true,
  },
  modules: [
    evlog(),
  ],
})
```

The `evlog/nitro/v3` module finds `evlog.config.ts` and bundles it into the server, with nothing to import. Enabling `asyncContext` lets you access the request-scoped logger from anywhere in the call stack via `useRequest()`.

### 4. 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:

```typescript [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.

<callout color="info" icon="i-custom-vite">

**Using Vite?** TanStack Start is Vite-based. The [`evlog/vite`](https://www.evlog.dev/reference/vite-plugin) plugin strips `log.debug()` from production builds and injects source locations, add it to your `vite.config.ts` alongside the TanStack Start plugin.

</callout>

## Wide Events

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

```typescript [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:

```bash [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-...
```

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

`useRequest()` is an experimental Nitro v3 feature powered by `AsyncLocalStorage`. It works on Node.js and Bun runtimes.

</callout>

## Background work

`log.fork()` is not available in this integration. The `evlog/nitro/v3` module attaches the logger to the request event and registers no `AsyncLocalStorage` storage of its own, so there is no scope for a child wide event to run in, even with `experimental.asyncContext` enabled (that powers Nitro's `useRequest()`, not `log.fork()`). Await background work inside the handler instead; mutating the logger after the wide event has emitted triggers a post-emit warning. See [After emit](https://www.evlog.dev/learn/wide-events#after-emit-sealing-and-background-work) in Wide events.

## Error Handling

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

<code-collapse>

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

</code-collapse>

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

```bash [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:

```tsx [src/routes/checkout.tsx]
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

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

Options passed to `evlog()` in `nitro.config.ts` override the file, merged key by key. The file's `drain`, `enrich` and `keep` are never overridden: they run alongside the `evlog:*` hooks, so an event reaches both.

## Route Filtering

Control which routes are logged with `include` and `exclude` in the file:

```typescript [evlog.config.ts]
export default defineEvlog({
  service: 'my-app',
  include: ['/api/**'],
  exclude: ['/_internal/**', '/health'],
  routes: {
    '/api/auth/**': { service: 'auth-service' },
    '/api/payment/**': { service: 'payment-service' },
  },
})
```

## Drain & Enrichers

Set `drain` and `enrich` in `evlog.config.ts`:

```typescript [evlog.config.ts]
import { defineEvlog } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'
import { createUserAgentEnricher } from 'evlog/enrichers'

const userAgent = createUserAgentEnricher()

export default defineEvlog({
  service: 'my-app',
  drain: createAxiomDrain(),
  enrich: (ctx) => {
    userAgent(ctx)
  },
})
```

A buffered drain from the file, such as one built with `createDrainPipeline`, is flushed when the server closes. Nitro plugins hooking `evlog:drain` and `evlog:enrich` also work and run alongside the file:

```typescript [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)
})
```

```typescript [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)
  })
})
```

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

See the [Adapters](https://www.evlog.dev/integrate/adapters/overview) and [Enrichers](https://www.evlog.dev/use-cases/enrichers) docs for all available drain adapters and enrichers.

</callout>

### Pipeline (Batching & Retry)

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

```typescript [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)
})
```

<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 the `evlog:emit:keep` hook to force-retain specific events regardless of head sampling:

```typescript [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

```bash [Terminal]
git clone https://github.com/evloghq/evlog.git
cd evlog/examples/tanstack-start
pnpm install
pnpm run dev
```

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

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

Browse the complete TanStack Start example source on GitHub.

</card>
</card-group>

## Next Steps

Deepen your **TanStack Start** 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/tanstack-start)


## Sitemap

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