Use the project specification and guidelines as you build the app.
Write the complete code for every step. Do not get lazy.
Your goal is to completely finish whatever I ask for.
- Server: Hono for server routes and API
- Following Hono Stacks for typed Env bindings
- Frontend:
- React + TanStack Router for SPA
- Tailwind CSS for utility-first styling
- shadcn for pre-built Tailwind-based UI components
- ORM: Drizzle for Postgres
- Icons: lucide-react
- Hosting: Should be easy to host on Cloudflare, AWS, or Vercel
Below is a recommended structure for your Hono + React (SPA) application with TanStack Router. Feel free to adapt it as needed but keep the separation of logic from presentation.
├─ server/ │ ├─ index.ts # Hono server entry point │ ├─ routes/ # Hono routes (REST endpoints, etc.) │ ├─ db/ # Drizzle setup/config (Postgres) │ └─ ... ├─ client/ │ ├─ main.tsx # ReactDOM.createRoot entry for the SPA │ ├─ App.tsx # Main React component for the app │ ├─ router/ │ │ ├─ index.ts # Exports the main TanStack Router instance or config │ │ ├─ root.route.ts # Example “root” route (TanStack Router) │ │ ├─ dash/ │ │ │ ├─ index.route.ts # Example route for /dash │ │ │ └─ settings.route.ts # Example route for /dash/settings │ │ └─ route-trees.ts # Example file exporting structured route trees │ ├─ logic/ # Business logic, custom hooks, data fetching │ ├─ components/ │ │ └─ ui/ # shadcn components (e.g., button.tsx, badge.tsx, etc.) │ ├─ lib/ # Client-side utility code │ ├─ assets/ # Static/media assets │ └─ ... ├─ types/ # Shared type definitions (client + server) │ ├─ index.ts # Barrel export for all types │ └─ ... ├─ vite.config.ts # Vite configuration ├─ .env.example # Example environment file ├─ .env.local # Local environment file (never commit) └─ ...
Follow these rules when building the project.
- Use
@to import anything from the project unless otherwise specified - End-to-End Type Safety
- Use kebab-case for filenames and folder names unless otherwise specified.
-
Environment Variables
- If you update environment variables, update the
.env.examplefile accordingly. - All environment variables should go in
.env.local(never committed). - Do not expose sensitive variables to the frontend.
- For Vite-based client-side env variables:
- Vite only exposes env variables prefixed with
VITE_. - Access them using
import.meta.env.VITE_SOME_VARIABLEin your client code. - Example
.env.local:VITE_API_URL="https://api.example.com" - Then in client code:
const apiUrl = import.meta.env.VITE_API_URL;
- Vite only exposes env variables prefixed with
- If you update environment variables, update the
-
Server-Side Access
- Access environment variables in Hono server code (using
process.env.VARIABLE_NAME), but do not leak secrets.
- Access environment variables in Hono server code (using
-
Type Importing
- When importing types, use
@/typesfor clarity. - Example:
import { MyType } from "@/types".
- When importing types, use
-
Filename Conventions
- Name type definition files like
example-types.ts. - All types should go in the
typesfolder; re-export them fromtypes/index.ts.
- Name type definition files like
-
Prefer Interfaces
- Prefer interfaces over type aliases for data shapes, unless a union type or advanced features require a type alias.
Example:
// types/actions-types.ts
export type ActionState<T> =
| { isSuccess: true; message: string; data: T }
| { isSuccess: false; message: string; data?: never };then export from types/index.ts
// types/index.ts
export * from './actions-types';We are building a Single Page Application (SPA) with TanStack Router and React. No React Server Components are required. Prefer shadcn components for UI elements.
-
Structure
- Put route definitions in
client/router/. - Keep logic (data fetching, API calls, state mgmt, custom hooks) in
client/logic/. - Store shadcn components in
client/components/ui/.
- Put route definitions in
-
Routing
- All routing is handled by TanStack Router. Keep your route definitions strongly typed.
- Use asynchronous data fetching as needed; prefer to keep business logic in
client/logic/or custom hooks.
-
Components
- Use
divunless another HTML tag is a better semantic fit. - Separate main parts of a component’s JSX with extra blank lines for clarity.
- Use
lucide-reactfor icons. - Keep presentational components “dumb” if possible (handle data manipulation in your logic layer).
- When using shadcn components, keep them in
client/components/ui/for consistency and ease of updates.
- Use
-
Fetching Data
- Use
useQueryfrom@tanstack/react-queryfor fetching data. - Use
useMutationfrom@tanstack/react-queryfor creating, updating, and deleting data. - In general, prefer to use
useQueryand render a shadcn skeleton component while the data is loading. Stay away fromuseEffectanduseStatefor data fetching.
- Use
Example:
// client/components/ui/example-component.tsx
import { Button } from '@/client/components/ui/button';
import { LucideIcon } from 'lucide-react';
interface ExampleProps {
items: string[];
}
export function ExampleComponent({ items }: ExampleProps) {
return (
<div className="p-4">
{items.map((item) => (
<div key={item}>{item}</div>
))}
<Button variant="default">
<LucideIcon name="check" />
Click Me
</Button>
</div>
);
}Since we use Hono for the server:
-
Hono Setup
- Create routes in
server/routes. - Mount them in
server/index.tsor a similar entry file:
import { Hono } from 'hono'; import { exampleRoute } from '@/server/routes/example-route'; // Example typed Env binding (see Hono Stacks docs) interface Bindings { DATABASE_URL: string; } const app = new Hono<Bindings>(); app.route('/example', exampleRoute); export default app;
- Create routes in
Follow the Hono Stacks for client server communication.
- Hono is used for the API.
Example:
const route = app.get(
'/hello',
zValidator('query', z.object({ name: z.string() })),
(c) => {
const { name } = c.req.valid('query');
return c.json({ message: `Hello! ${name}` });
}
);- Validate with Zod to receive the value of the query parameter.
Example:
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
app.get(
'/hello',
zValidator(
'query',
z.object({
name: z.string()
})
),
(c) => {
const { name } = c.req.valid('query');
return c.json({
message: `Hello! ${name}`
});
}
);- Sharing the Types
- To emit an endpoint specification, export its type.
const route = app.get(
'/hello',
zValidator(
'query',
z.object({
name: z.string()
})
),
(c) => {
const { name } = c.req.valid('query');
return c.json({
message: `Hello! ${name}`
});
}
);
export type AppType = typeof route;- Create a client object by passing the AppType type to hc as generics.
import { AppType } from './server';
import { hc } from 'hono/client';
const client = hc<AppType>('/api');
const res = await client.hello.$get({
query: {
name: 'Hono'
}
});Client Server Example with Hono and React
API Server:
// functions/api/[[route]].ts
import { Hono } from 'hono';
import { handle } from 'hono/cloudflare-pages';
import { z } from 'zod';
import { zValidator } from '@hono/zod-validator';
const app = new Hono();
const schema = z.object({
id: z.string(),
title: z.string()
});
type Todo = z.infer<typeof schema>;
const todos: Todo[] = [];
const route = app
.post('/todo', zValidator('form', schema), (c) => {
const todo = c.req.valid('form');
todos.push(todo);
return c.json({
message: 'created!'
});
})
.get((c) => {
return c.json({
todos
});
});
export type AppType = typeof route;
export const onRequest = handle(app, '/api');The client with React and React Query:
// src/App.tsx
import {
useQuery,
useMutation,
QueryClient,
QueryClientProvider
} from '@tanstack/react-query';
import { AppType } from '../functions/api/[[route]]';
import { hc, InferResponseType, InferRequestType } from 'hono/client';
const queryClient = new QueryClient();
const client = hc<AppType>('/api');
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Todos />
</QueryClientProvider>
);
}
const Todos = () => {
const query = useQuery({
queryKey: ['todos'],
queryFn: async () => {
const res = await client.todo.$get();
return await res.json();
}
});
const $post = client.todo.$post;
const mutation = useMutation<
InferResponseType<typeof $post>,
Error,
InferRequestType<typeof $post>['form']
>(
async (todo) => {
const res = await $post({
form: todo
});
return await res.json();
},
{
onSuccess: async () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
onError: (error) => {
console.log(error);
}
}
);
return (
<div>
<button
onClick={() => {
mutation.mutate({
id: Date.now().toString(),
title: 'Write code'
});
}}
>
Add Todo
</button>
<ul>
{query.data?.todos.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
</div>
);
};- All database operation should go through drizzle ORM on the server.
- Use the
db/schema.tsfile to define the database schema. - Use the
createdAtandupdatedAtcolumns to track when a row was created and last updated. - When creating a relationship, always create a many to many relationship, even if it is a one to one relationship.