---
title: "Build an app with Tanstack and Deno"
description: "Complete guide to building applications with Tanstack and Deno. Learn how to implement Query for data fetching, Router for navigation, manage server state, and create type-safe full-stack applications."
url: /examples/tanstack_tutorial/
---
[Tanstack](https://tanstack.com/) is a set of framework-agnostic data management
tools. With Tanstack, developers can manage server state efficiently with
[Query](https://tanstack.com/query/latest), create powerful tables with
[Table](https://tanstack.com/table/latest), handle complex routing with
[Router](https://tanstack.com/router/latest), and build type-safe forms with
[Form](https://tanstack.com/form/latest). These tools work seamlessly across
[React](/examples/react_tutorial), [Vue](/examples/vue_tutorial),
[Solid](/examples/solidjs_tutorial), and other frameworks while maintaining
excellent TypeScript support.
In this tutorial, we’ll build a simple app using
[Tanstack Query](https://tanstack.com/query/latest) and
[Tanstack Router](https://tanstack.com/router/latest/docs/framework/react/quick-start).
The app will display a list of dinosaurs. When you click on one, it'll take you
to a dinosaur page with more details.
- [Start with the backend API](#start-with-the-backend-api)
- [Create a Tanstack-driven frontend](#create-tanstack-driven-frontend)
- [Next steps](#next-steps)
Feel free to skip directly to
[the source code](https://github.com/denoland/examples/tree/main/with-tanstack)
or follow along below!
## Start with the backend API
Within our main directory, let's setup an `api/` directory and create our
dinosaur data file, `api/data.json`:
```jsonc
// api/data.json
[
{
"name": "Aardonyx",
"description": "An early stage in the evolution of sauropods."
},
{
"name": "Abelisaurus",
"description": "\"Abel's lizard\" has been reconstructed from a single skull."
},
{
"name": "Abrictosaurus",
"description": "An early relative of Heterodontosaurus."
},
...
]
```
This is where our data will be pulled from. In a full application, this data
would come from a database.
> ⚠️️ In this tutorial we hard code the data. But you can connect
> to [a variety of databases](https://docs.deno.com/runtime/tutorials/connecting_to_databases/) and [even use ORMs like Prisma](https://docs.deno.com/runtime/tutorials/how_to_with_npm/prisma/) with
> Deno.
Secondly, let's create our [Hono](https://hono.dev/) server. We start by
installing Hono from [JSR](https://jsr.io) with `deno add`:
```shell
deno add jsr:@hono/hono
```
Next, let's create an `api/main.ts` file and populate it with the below. Note
we'll need to import
[`@hono/hono/cors`](https://hono.dev/docs/middleware/builtin/cors) and define
key attributes to allow the frontend to access the API routes.
```ts
// api/main.ts
import { Hono } from "@hono/hono";
import { cors } from "@hono/hono/cors";
import data from "./data.json" with { type: "json" };
const app = new Hono();
app.use(
"/api/*",
cors({
origin: "http://localhost:5173",
allowMethods: ["GET", "POST", "PUT", "DELETE"],
allowHeaders: ["Content-Type", "Authorization"],
exposeHeaders: ["Content-Type", "Authorization"],
credentials: true,
maxAge: 600,
}),
);
app.get("/", (c) => {
return c.text("Welcome to the dinosaur API!");
});
app.get("/api/dinosaurs", (c) => {
return c.json(data);
});
app.get("/api/dinosaurs/:dinosaur", (c) => {
if (!c.req.param("dinosaur")) {
return c.text("No dinosaur name provided.");
}
const dinosaur = data.find((item) =>
item.name.toLowerCase() === c.req.param("dinosaur").toLowerCase()
);
if (dinosaur) {
return c.json(dinosaur);
} else {
return c.notFound();
}
});
Deno.serve(app.fetch);
```
The Hono server provides two API endpoints:
- `GET /api/dinosaurs` to fetch all dinosaurs, and
- `GET /api/dinosaurs/:dinosaur` to fetch a specific dinosaur by name
Before we start working on the frontend, let's update our `deno tasks` in our
`deno.json` file. Yours should look something like this:
```jsonc
{
"tasks": {
"dev": "deno --allow-env --allow-net api/main.ts"
}
// ...
}
```
Now, the backend server will be started on `localhost:8000` when we run
`deno task dev`.
## Create Tanstack-driven frontend
Let's create the frontend that will use this data. First, we'll quickly scaffold
a new React app with Vite using the TypeScript template in the current
directory:
```shell
deno init --npm vite@latest --template react-ts ./
```
Then, we'll install our Tanstack-specific dependencies:
```shell
deno install npm:@tanstack/react-query npm:@tanstack/react-router
```
Let's update our `deno tasks` in our `deno.json` to add a command to start the
Vite server:
```jsonc
// deno.json
{
"tasks": {
"dev": "deno task dev:api & deno task dev:vite",
"dev:api": "deno --allow-env --allow-net api/main.ts",
"dev:vite": "deno -A npm:vite"
}
// ...
}
```
We can move onto building our components. We'll need two main pages for our app:
- `DinosaurList.tsx`: the index page, which will list out all the dinosaurs, and
- `Dinosaur.tsx`: the leaf page, which displays information about a single
dinosaur
Let's create a new `./src/components` directory and, within that, the file
`DinosaurList.tsx`:
```ts
// ./src/components/DinosaurList.tsx
import { useQuery } from "@tanstack/react-query";
import { Link } from "@tanstack/react-router";
async function fetchDinosaurs() {
const response = await fetch("http://localhost:8000/api/dinosaurs");
if (!response.ok) {
throw new Error("Failed to fetch dinosaurs");
}
return response.json();
}
export function DinosaurList() {
const {
data: dinosaurs,
isLoading,
error,
} = useQuery({
queryKey: ["dinosaurs"],
queryFn: fetchDinosaurs,
});
if (isLoading) return
);
}
```
This uses
[`useQuery`](https://tanstack.com/query/v4/docs/framework/react/guides/queries)
from **Tanstack Query** to fetch and cache the dinosaur data automatically, with
built-in loading and error states. Then it uses
[`Link`](https://tanstack.com/router/v1/docs/framework/react/api/router/linkComponent)
from **Tanstack Router** to create client-side navigation links with type-safe
routing parameters.
Next, let's create the `DinosaurDetail.tsx` component in the `./src/components/`
folder, which will show details about a single dinosaur:
```ts
// ./src/components/DinosaurDetail.tsx
import { useParams } from "@tanstack/react-router";
import { useQuery } from "@tanstack/react-query";
async function fetchDinosaurDetail(name: string) {
const response = await fetch(`http://localhost:8000/api/dinosaurs/${name}`);
if (!response.ok) {
throw new Error("Failed to fetch dinosaur detail");
}
return response.json();
}
export function DinosaurDetail() {
const { name } = useParams({ from: "/dinosaur/$name" });
const {
data: dinosaur,
isLoading,
error,
} = useQuery({
queryKey: ["dinosaur", name],
queryFn: () => fetchDinosaurDetail(name),
});
if (isLoading) return
Loading...
;
if (error instanceof Error) {
return
An error occurred: {error.message}
;
}
return (
{name}
{dinosaur?.description}
);
}
```
Again, this uses `useQuery` from **Tanstack Query** to fetch and cache
individual dinosaur details, with
[`queryKey`](https://tanstack.com/query/latest/docs/framework/react/guides/query-keys)
including the dinosaur name to ensure proper caching. Additionally, we use
[`useParams`](https://tanstack.com/router/v1/docs/framework/react/api/router/useParamsHook)
from **Tanstack Router** to safely extract and type the URL parameters defined
in our route configuration.
Before we can run this, we need to encapsulate these components into a layout.
Let's create another file in the `./src/components/` folder called `Layout.tsx`:
```ts
// ./src/components/Layout.tsx
export function Layout() {
return (
Dinosaur Encyclopedia
);
}
```
You may notice the
[`Outlet`](https://tanstack.com/router/v1/docs/framework/react/guide/outlets)
component towards the bottom of our newly created layout. This component is from
**Tanstack Router** and renders the child route's content, allowing for nested
routing while maintaining a consistent layout structure.
Next, we'll have to wire up this layout with `./src/main.tsx`, which an
important file that sets up the Tanstack Query client for managing server state
and the Tanstack Router for handling navigation:
```ts
// ./src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createRouter, RouterProvider } from "@tanstack/react-router";
import { routeTree } from "./routeTree";
const queryClient = new QueryClient();
const router = createRouter({ routeTree });
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
ReactDOM.createRoot(document.getElementById("root")!).render(
,
);
```
You'll notice we import
[`QueryClientProvider`](https://tanstack.com/query/latest/docs/framework/react/reference/QueryClientProvider),
which wraps the entire application to allow for query caching and state
management. We also import `RouterProvider`, which connects our defined routes
to React's rendering system.
Finally, we'll need to define a
[`routeTree.tsx`](https://tanstack.com/router/v1/docs/framework/react/guide/route-trees)
file in our `./src/` directory. This file defines our application's routing
structure using Tanstack Router's type-safe route definitions:
```ts
// ./src/routeTree.tsx
import { RootRoute, Route } from "@tanstack/react-router";
import { DinosaurList } from "./components/DinosaurList";
import { DinosaurDetail } from "./components/DinosaurDetail";
import { Layout } from "./components/Layout";
const rootRoute = new RootRoute({
component: Layout,
});
const indexRoute = new Route({
getParentRoute: () => rootRoute,
path: "/",
component: DinosaurList,
});
const dinosaurRoute = new Route({
getParentRoute: () => rootRoute,
path: "dinosaur/$name",
component: DinosaurDetail,
});
export const routeTree = rootRoute.addChildren([indexRoute, dinosaurRoute]);
```
In `./src/routeTree.tsx`, we create a hierarchy of routes with `Layout` as the
root component. Then we set two child routes, their paths and components — one
for the dinosaur list, `DinosaurList`, and the other for the individual dinosaur
details with a dynamic parameter, `DinosaurDetail`.
With all that complete, we can run this project:
```shell
deno task dev
```
## Next steps
This is just the beginning of building with Deno and Tanstack. You can add
persistent data storage like
[using a database like Postgres or MongoDB](https://docs.deno.com/runtime/tutorials/connecting_to_databases/)
and an ORM like [Drizzle](https://deno.com/blog/build-database-app-drizzle) or
[Prisma](https://docs.deno.com/runtime/tutorials/how_to_with_npm/prisma/). Or
deploy your app to
[AWS](https://docs.deno.com/runtime/tutorials/aws_lightsail/),
[Digital Ocean](https://docs.deno.com/runtime/tutorials/digital_ocean/), or
[Google Cloud Run](https://docs.deno.com/runtime/tutorials/google_cloud_run/)
You could also add real-time updates using
[Tanstack Query's refetching capabilities](https://tanstack.com/query/latest/docs/framework/react/examples/auto-refetching),
[implement infinite scrolling](https://tanstack.com/query/latest/docs/framework/react/examples/load-more-infinite-scroll)
for large dinosaur lists, or
[add complex filtering and sorting](https://tanstack.com/table/v8/docs/guide/column-filtering)
using **[Tanstack Table](https://tanstack.com/table/latest)**. The combination
of Deno's built-in web standards, tooling, and native TypeScript support, as
well as Tanstack's powerful data management opens up numerous possibilities for
building robust web applications.