TanStack Start + Hono Full-Stack Architecture
TanStack Start is a full-stack React framework with SSR, file-based routing, and server functions. Hono is an ultrafast, lightweight web framework that runs on any JavaScript runtime. Combining them in a monorepo gives you a modern, type-safe full-stack stack.
badmin Admin panel — TanStack Start + Hono + Drizzle + SQLite.
cyop AI captioning platform — TanStack Router + Hono + tRPC + Drizzle + Postgres.
Architecture Overview
monorepo/
├── apps/
│ ├── web/ # TanStack Start (React + SSR)
│ ├── server/ # Hono API server
│ └── docs/ # Fumadocs documentation site
├── packages/
│ ├── contracts/ # Shared Zod schemas + tRPC routers
│ ├── ui/ # Shared UI components (shadcn/ui)
│ ├── db/ # Drizzle schema + migrations
│ └── auth/ # Better Auth configuration
Frontend: TanStack Start
TanStack Start provides file-based routing with @tanstack/react-router, SSR via Vinxi, and full-stack server functions.
Key Setup
// apps/web/package.json
{
"dependencies" : {
"@tanstack/react-start" : "latest" ,
"@tanstack/react-router" : "latest" ,
"@tanstack/router-plugin" : "latest"
}
}
Route Configuration
// apps/web/app/routes/__root.tsx
import { createRootRoute, Outlet } from "@tanstack/react-router"
export const Route = createRootRoute ({
component: RootLayout,
})
function RootLayout () {
return (
< div >
< Navbar />
< Outlet />
</ div >
)
}
Server Functions
TanStack Start supports server-side functions that run only on the server:
// apps/web/app/routes/dashboard.tsx
import { createServerFn } from "@tanstack/react-start"
import { authMiddleware } from "../middleware"
export const getDashboardData = createServerFn ({ method: "GET" })
. middleware ([authMiddleware])
. handler ( async () => {
return await db.query. dashboardStats ()
})
Use server functions for data fetching and mutations that need server-side execution. They automatically handle serialization and avoid leaking secrets to the client.
Backend: Hono
Hono is the API layer — fast, type-safe, and runtime-agnostic. It handles auth, CRUD, and business logic.
Basic Server Setup
// apps/server/src/app.ts
import { Hono } from "hono"
import { cors } from "hono/cors"
import { logger } from "hono/logger"
import { sessionMiddleware } from "./middleware/session"
import { authRoutes } from "./modules/auth/auth.routes"
import { userRoutes } from "./modules/users/users.routes"
const app = new Hono ()
app. use ( "*" , logger ())
app. use ( "*" , cors ({ origin: [ "http://localhost:3000" ], credentials: true }))
app. use ( "/api/*" , sessionMiddleware (authService))
app. route ( "/api/auth" , authRoutes)
app. route ( "/api/users" , userRoutes)
export default app
Service Pattern
Use factory functions for dependency injection:
// apps/server/src/modules/auth/auth.service.ts
interface AuthServiceOptions {
userRepository : AuthUserRepository
hashPassword : ( password : string ) => Promise < string >
verifyPassword : ( password : string , hash : string ) => Promise < boolean >
}
export function createAuthService ( options : AuthServiceOptions ) {
return {
async login ( input : LoginInput ) {
const user = await options.userRepository. findForAuthByEmail (input.email)
if ( ! user) throw new HTTPException ( 401 , { message: "Invalid credentials" })
const valid = await options. verifyPassword (input.password, user.passwordHash)
if ( ! valid) throw new HTTPException ( 401 , { message: "Invalid credentials" })
await options.userRepository. touchLastLogin (user.id)
return toAuthSession (user)
},
}
}
Factory functions (createXxxService) give you explicit dependency injection without class boilerplate. Each service receives its dependencies as parameters, making testing trivial — just pass mock implementations.
Session Cookie Auth
Instead of JWT tokens, use signed session cookies for simplicity and security:
// apps/server/src/middleware/session.ts
import { getSignedCookie, setSignedCookie, deleteCookie } from "hono/cookie"
const SESSION_COOKIE = "app_session"
const COOKIE_MAX_AGE = 60 * 60 * 24 * 7 // 7 days
export async function setSessionCookie ( ctx : Context , userId : string , version : number ) {
await setSignedCookie (ctx, SESSION_COOKIE , `${ userId }:${ version }` , secret, {
httpOnly: true ,
maxAge: COOKIE_MAX_AGE ,
sameSite: "Lax" ,
secure: isProduction,
})
}
export function requireSession ( authService : AuthService ) {
return createMiddleware ( async ( c , next ) => {
const value = await getSignedCookie (c, secret, SESSION_COOKIE )
if ( ! value) throw new HTTPException ( 401 , { message: "Login required" })
const payload = parseSessionCookie (value)
const session = await authService. getSession (payload)
if ( ! session) {
deleteCookie (c, SESSION_COOKIE )
throw new HTTPException ( 401 , { message: "Session expired" })
}
c. set ( "sessionUser" , session.user)
await next ()
})
}
export function requirePermission ( permission : string ) {
return createMiddleware ( async ( c , next ) => {
const user = c. get ( "sessionUser" )
if ( ! user.permissions. includes (permission)) {
throw new HTTPException ( 403 , { message: "Permission denied" })
}
await next ()
})
}
Session versioning lets you force logout users when their password changes or account is disabled. Increment sessionVersion on the user record — stale cookies will be rejected.
Localized Error Responses
For bilingual apps, return locale-aware error messages from the API:
// apps/server/src/lib/i18n.ts
import { HTTPException } from "hono/http-exception"
const messages = {
en: { "auth.invalidCredentials" : "Invalid email or password" },
zh: { "auth.invalidCredentials" : "邮箱或密码错误" },
}
export class LocalizedHTTPException extends HTTPException {
constructor ( status : number , key : string ) {
super (status, { message: key }) // key is resolved by frontend
}
}
// Frontend sends X-Locale header
// Server resolves the key to the appropriate language
Styling: UnoCSS
For TanStack Start projects, UnoCSS is lighter and faster than Tailwind for utility classes:
// vite.config.ts
import UnoCSS from "unocss/vite"
import presetWind4 from "@unocss/preset-wind4"
import presetIcons from "@unocss/preset-icons"
export default defineConfig ({
plugins: [
UnoCSS ({
presets: [ presetWind4 (), presetIcons ()],
}),
],
})
Use presetWind4 for Tailwind CSS 4-compatible utilities. Use presetIcons for FontAwesome/Material icon classes without importing SVGs.
Best Practices
Monorepo with shared contracts — Put Zod schemas and tRPC routers in a shared package so frontend and backend stay type-synced
Factory-function services — Prefer createAuthService(opts) over class AuthService for explicit DI
Signed cookie sessions — Simpler than JWT, revocable by bumping sessionVersion
Server functions for secrets — Never expose API keys or DB queries in client code
Locale-aware errors — Frontend sends X-Locale, server returns localized keys
UnoCSS over Tailwind — Faster build, icon presets, and Tailwind-compatible utilities
References