Nitro V3 + Tanstack Router + React

Nitro V3 is out now with big improvements. Nitro now comes as a Vite plugin, so you can use it inside any Vite project. You can also bring your own backend framework and run it through Nitro. This means you can use tools like tRPC, Elysia, or Hono with the server entry point and build full stack apps using Vite.

By default, Nitro V3 uses rendu for templates. In this guide, we’ll replace rendu with React + TanStack Router.

Let’s start by creating a new Nitro V3 project. Make sure to pick the Full Stack With Vite option.

Terminal window
bunx create-nitro-app

Go into your new project and install the needed packages for React and TanStack Router:

Terminal window
bun add react react-dom @tanstack/react-router @tanstack/react-router-devtools
bun add -D @tanstack/router-plugin @types/react @types/react-dom @vitejs/plugin-react

After installing, add the TanStack Router and React plugins to your vite.config.ts file with the nitro plugin:

vite.config.ts
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";
import react from "@vitejs/plugin-react";
import { tanstackRouter } from "@tanstack/router-plugin/vite";
export default defineConfig({
plugins: [
tanstackRouter({
target: "react",
autoCodeSplitting: true,
}),
react(),
nitro(),
],
nitro: {
preset: "standard",
},
});

Next, update your tsconfig.json file to support JSX, add Vite types, and set up import aliases for the src directory.

tsconfig.json
{
"compilerOptions": {
// Module resolution
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
// JSX
"jsx": "preserve",
"jsx": "react-jsx",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment",
// Core checks
"strict": true,
"noEmit": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
// Additional safety
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"useUnknownInCatchVariables": true,
"noUnusedLocals": true,
"types": ["vite/client"],
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src", "types.d.ts"],
"exclude": ["node_modules", "dist"]
}

You should already have a src folder in your project. We need to create three files inside it to set up Tanstack Router.

  • src/routes/__root.tsx (with two ’_’ characters)
  • src/routes/index.tsx
  • src/main.tsx (there should already be a src/main.ts file, we’ll rename it to src/main.tsx)

src/routes/__root.tsx is the root route.

src/routes/__root.tsx
import { createRootRoute, Link, Outlet } from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
const RootLayout = () => (
<>
<div className="p-2 flex gap-2">
<Link to="/" className="[&.active]:font-bold">
Home
</Link>{' '}
<Link to="/about" className="[&.active]:font-bold">
About
</Link>
</div>
<hr />
<Outlet />
<TanStackRouterDevtools />
</>
)
export const Route = createRootRoute({ component: RootLayout })

src/routes/index.tsx is the index route. You can see it makes a request to /api/hello which is already set up in the routes/api/hello.ts file.

src/routes/index.tsx
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/")({
loader: async () => {
const r = await fetch("/api/hello");
return r.json();
},
component: Index,
});
function Index() {
const r = Route.useLoaderData();
return (
<div className="p-2">
<h3>{JSON.stringify(r)}</h3>
</div>
);
}

src/main.tsx is the main entry point. Rename src/main.ts to src/main.tsx and add the code below. You can delete the src/app.ts file now since we don’t need it anymore.

src/main.tsx
import { StrictMode } from 'react'
import ReactDOM from 'react-dom/client'
import { RouterProvider, createRouter } from '@tanstack/react-router'
// Import the generated route tree
import { routeTree } from './routeTree.gen'
// Create a new router instance
const router = createRouter({ routeTree })
// Register the router instance for type safety
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
// Render the app
const rootElement = document.getElementById('root')!
if (!rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement)
root.render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>,
)
}

Update the routes/api/hello.ts file to return a JSON response.

routes/api/hello.ts
import { defineHandler } from "nitro/h3";
export default defineHandler((event) => {
return { hello: "API" };
});

Update the index.html file in the project root to use the main.tsx file.

index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My Nitro + TanStack Router + React App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

When you’re done with all the changes, run bun run dev to start the development server. Go to http://localhost:3000 and you should see the app running with the Hello API message.