# TanStack Start Basics Part 2 — Connecting to Your Directus Backend

%[https://www.youtube.com/watch?v=BUULSpRQiAY] 

Our frontend has been static so far — just hardcoded headings on a few pages. Today we connect it to the Directus backend built earlier in this series, using the official Directus SDK to pull real product data into our TanStack Start app.

## Installing the Directus SDK

There are a few ways to talk to Directus from a frontend — the REST API directly, GraphQL — but the SDK is the cleanest option, and what we're using here.

```bash
npm install @directus/sdk
```

## Creating the Directus Helper

Inside `src/lib`, create a new file: `directus.ts`.

```typescript
import { createDirectus, rest } from '@directus/sdk'

export const directus = createDirectus('Your_Directus_Project_URL').with(rest())
```

`createDirectus` initializes a connection client between your app and your Directus instance. On its own it starts empty — capabilities get added by chaining `.with()`. Since we're fetching over standard HTTP REST requests, we pair it with the `rest()` modifier.

## Defining Our First Type

Inside `src`, create `types/index.ts`:

```typescript
export interface Products {
  id: string
  name: string
  brief: string
  description: string
  price: number
  sale: number
  quantity: number
  isOnSale: boolean
  category: string
  image: string
}
```

## Writing the Fetch Function

Back in `directus.ts`:

```typescript
import { createDirectus, rest, readItems } from '@directus/sdk'
import type { Products } from '@/types'

export const directus = createDirectus('Your_Directus_Project_URL').with(rest())

export async function getProducts(): Promise<Products[]> {
  return directus.request(
    readItems('products', {
      fields: [
        'id',
        'name',
        'brief',
        'description',
        'price',
        'sale',
        'quantity',
        'isOnSale',
        'category',
        'image',
      ],
    }),
  )
}
```

`readItems` is a built-in SDK helper that generates a request for fetching multiple items from a given collection. At this point, TypeScript throws:

```plaintext
Type 'Record<string, any>[]' is not assignable to type 'Products[]'.
```

## Fixing the Schema Error

This happens because the SDK doesn't automatically know your database's actual shape — it needs a `Schema` type passed into `createDirectus`:

```typescript
type Schema = {
  products: Products[]
}

export const directus = createDirectus<Schema>('Your_Directus_Project_URL').with(rest())
```

This declares that a `products` collection exists and maps to an array of `Products`. With this in place, the error resolves — and as a bonus, TypeScript now validates and autocompletes the `fields` array against this type too.

## Fetching the Data — Method 1: TanStack Query

Install it:

```bash
npm i @tanstack/react-query
```

Set it up in `__root.tsx`:

```tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

// Create a client
const queryClient = new QueryClient()

function RootDocument({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <HeadContent />
      </head>
      <body>
        <QueryClientProvider client={queryClient}>
          {children}
        </QueryClientProvider>
        <Scripts />
      </body>
    </html>
  )
}
```

> **Why does** `RootDocument` **take a** `children` **prop, and why wrap it?** `__root.tsx` wraps your entire application — every route (About, Index, Products) gets passed in as `children`, rendered inside this HTML shell. Wrapping `children` with the query provider means every nested route and component now has access to TanStack Query.

Now build the component, `src/components/Products.tsx`:

```tsx
import { useQuery } from '@tanstack/react-query'

export default function Products() {
  const { data: products, isLoading: loading } = useQuery({
    queryKey: ['product'],
    queryFn: getProducts,
    staleTime: 5 * 60 * 1000,
    gcTime: 15 * 60 * 1000,
    refetchOnWindowFocus: false,
    retry: 2,
    refetchOnMount: true,
  })

  if (loading) {
    return <div className="container mx-auto p-8">Loading...</div>
  }

  return (
    <div>
      <div className="container mx-auto p-8">
        <h1 className="text-4xl font-medium mb-4">Products</h1>
        <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
          {products?.map((product) => (
            <div key={product.id} className="border rounded-lg p-4">
              <h2 className="text-xl font-bold mb-2">{product.name}</h2>
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}
```

A quick rundown of the query options:

| Option | What it does |
| --- | --- |
| `staleTime` | How long fetched data stays "fresh" (5 min) before a refetch is considered |
| `gcTime` | How long unused data stays cached in memory (15 min) before being cleared |
| `refetchOnWindowFocus` | Whether switching tabs and back triggers a refetch (off here) |
| `retry` | Silent retry attempts on failure before giving up (2 here) |
| `refetchOnMount` | Whether the component checks for stale data and refetches on every mount (on here) |

Wire it into the route:

```tsx
import Products from '@/components/Products'
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/products')({
  component: RouteComponent,
})

function RouteComponent() {
  return (
    <div className="p-8">
      <h1 className="font-bold text-5xl text-slate-700">Products</h1>
      <Products />
    </div>
  )
}
```

Visit `localhost:3000/products` — real product data, pulled live from Directus.

## Fetching the Data — Method 2: Route Loaders

Loaders work a little differently, and only on routes directly — not on standalone components. Remove the `Products` component from the route and build it in directly:

```tsx
import { createFileRoute } from '@tanstack/react-router'
import { getProducts } from '@/lib/directus'

export const Route = createFileRoute('/products')({
  loader: async () => {
    const products = await getProducts()
    return products
  },
  component: RouteComponent,
})

function RouteComponent() {
  const products = Route.useLoaderData()

  return (
    <div className="p-8">
      <h1 className="font-bold text-5xl text-slate-700 mb-4">Products</h1>
      <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
        {products?.map((product) => (
          <div key={product.id} className="border rounded-lg p-4">
            <h2 className="text-xl font-bold mb-2">{product.name}</h2>
          </div>
        ))}
      </div>
    </div>
  )
}
```

The loader runs `getProducts` before the route renders, and `Route.useLoaderData()` pulls that resolved data straight into the component — no loading state needed, since the data's already there by the time the page shows up.

## Which One Should You Use?

Both approaches call the exact same `getProducts` function underneath — the difference is *where* the fetching happens and *how* the result reaches your component.

**TanStack Query** shines for data that changes often or benefits from caching, background refetching, and built-in loading states — great for a snappy feel on repeat visits.

**Route loaders** fetch before the route even renders, so there's no loading spinner at all. That pairs particularly well with TanStack Start's server-side rendering model, since the data can be resolved as part of the initial render rather than after the fact.

## Summary

In this part, we:

*   Installed and configured the Directus SDK
    
*   Typed our data with a `Products` interface and a `Schema`
    
*   Built our first real fetch function using `readItems`
    
*   Rendered a live products grid two different ways — TanStack Query and a route loader
    

Right now we're only displaying each product's name, but this same pattern carries directly into fuller product cards next — pricing, images, sale badges, and eventually branding and tags once those relational fields come into play.

* * *

*Found this useful? Follow for more Directus and TanStack Start tutorials, or check out the video version above.*
