Skip to main content

Building a Resource Management View

In this tutorial, you will build a fully functional resource management dashboard using the pre-built components and services in this template. You will learn how to fetch data using the generated SDK, display it in a data table, and implement CRUD (Create, Read, Update, Delete) operations.

By the end of this guide, you will have a dashboard that lists items, allows for new item creation, and provides a menu for editing or deleting existing entries.

Prerequisites

Before starting, ensure you have the following configured in your project:

  • TanStack Query: For data fetching and state management.
  • TanStack Router: For route handling.
  • Generated SDK: The ItemsService must be generated and available in frontend/src/client.

Step 1: Fetching Data with ItemsService

The first step is to retrieve your data from the backend. You will use the ItemsService.readItems method within a TanStack Query.

In frontend/src/routes/_layout/items.tsx, define your query options and use the useSuspenseQuery hook:

import { useSuspenseQuery } from "@tanstack/react-query"
import { ItemsService } from "@/client"

function getItemsQueryOptions() {
return {
queryFn: () => ItemsService.readItems({ skip: 0, limit: 100 }),
queryKey: ["items"],
}
}

function ItemsTableContent() {
// This hook automatically handles the loading state via React Suspense
const { data: items } = useSuspenseQuery(getItemsQueryOptions())

return <DataTable columns={columns} data={items.data} />
}

The ItemsService.readItems method is a static call that returns a CancelablePromise. By wrapping it in getItemsQueryOptions, you create a reusable configuration that TanStack Query uses to manage caching and background updates.

Step 2: Defining the Table Structure

To display the fetched data, you need to define how each column should look. This is handled in frontend/src/components/Items/columns.tsx using the ColumnDef type from @tanstack/react-table.

import type { ColumnDef } from "@tanstack/react-table"
import type { ItemPublic } from "@/client"
import { ItemActionsMenu } from "./ItemActionsMenu"

export const columns: ColumnDef<ItemPublic>[] = [
{
accessorKey: "title",
header: "Title",
cell: ({ row }) => (
<span className="font-medium">{row.original.title}</span>
),
},
{
accessorKey: "description",
header: "Description",
cell: ({ row }) => (
<span className="max-w-xs truncate block text-muted-foreground">
{row.original.description || "No description"}
</span>
),
},
{
id: "actions",
cell: ({ row }) => (
<div className="flex justify-end">
<ItemActionsMenu item={row.original} />
</div>
),
},
]

In this configuration, the actions column is used to inject the ItemActionsMenu component into every row, passing the current item data as a prop.

Step 3: Implementing Row Actions

The ItemActionsMenu component provides a dropdown for managing individual resources. It acts as a container for EditItem and DeleteItem components.

In frontend/src/components/Items/ItemActionsMenu.tsx:

import { useState } from "react"
import { EllipsisVertical } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { EditItem } from "./EditItem"
import { DeleteItem } from "./DeleteItem"

export const ItemActionsMenu = ({ item }: ItemActionsMenuProps) => {
const [open, setOpen] = useState(false)

return (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<EllipsisVertical />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<EditItem item={item} onSuccess={() => setOpen(false)} />
<DeleteItem id={item.id} onSuccess={() => setOpen(false)} />
</DropdownMenuContent>
</DropdownMenu>
)
}

This component uses a local open state to control the dropdown. When an action like "Edit" or "Delete" succeeds, the onSuccess callback closes the menu.

Step 4: Creating New Resources

To add new items to your dashboard, use the AddItem component. This component combines a Shadcn UI Dialog, react-hook-form for validation, and a TanStack Query mutation.

In frontend/src/components/Items/AddItem.tsx, the mutation is defined as follows:

const queryClient = useQueryClient()

const mutation = useMutation({
mutationFn: (data: ItemCreate) =>
ItemsService.createItem({ requestBody: data }),
onSuccess: () => {
showSuccessToast("Item created successfully")
form.reset()
setIsOpen(false)
},
onError: handleError.bind(showErrorToast),
onSettled: () => {
// This is critical for UI synchronization
queryClient.invalidateQueries({ queryKey: ["items"] })
},
})

const onSubmit = (data: FormData) => {
mutation.mutate(data)
}

When the form is submitted, ItemsService.createItem sends a POST request to the API. The onSettled callback ensures that the ["items"] query is invalidated, forcing the table to refetch and display the newly created item immediately.

Step 5: Final Assembly

Finally, combine these components in your main route component in frontend/src/routes/_layout/items.tsx:

function Items() {
return (
<div className="flex flex-col gap-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold tracking-tight">Items</h1>
<p className="text-muted-foreground">Create and manage your items</p>
</div>
<AddItem />
</div>
<Suspense fallback={<PendingItems />}>
<ItemsTableContent />
</Suspense>
</div>
)
}

By wrapping ItemsTableContent in Suspense, you provide a smooth loading experience while ItemsService.readItems is fetching data. The AddItem button is placed at the top level, providing a clear entry point for resource creation.

Summary of the Pattern

This implementation follows a consistent pattern used throughout the template:

  1. Service Layer: ItemsService handles all raw API communication.
  2. Query Layer: TanStack Query (useSuspenseQuery, useMutation) manages the server state.
  3. UI Layer: Shadcn UI components (DataTable, Dialog, DropdownMenu) provide the visual structure.
  4. Synchronization: queryClient.invalidateQueries ensures the UI stays in sync with the backend after any modification.