
Kyle Austad
2026-09-29
I was starting a new project and was tired of using the same old stack. I've heard good things about Hono but wasn't quite ready to go back to Vue for the frontend as ShadCn is just too first class in React. So to switch up the usual NextJs I wanted to structure my new project with separate frontend and backends. The frontend being Vite + React and the backend being something entirely new to me, Hono. I've heard really good things about Hono and how fast it is, and the documentation showed a DX and syntax really similar feeling to Express but modernized with types. I also recently started using Bun as my JS runtime as well as NPM client and have been loving it.
Now microservices usually go hand-in-hand with monorepos but they aren't mutually exclusive. Microservices are more about small, singular focused code-bases that own a specific function or domain of your project. Monorepos are a way to organize separate codebases into a singular git repository while maintaining their functional separation, but still making it much easier for them to share things like types, which Hono advertises on their docs as being first class across backend and frontend. My project is not microservices. I have a monolithic backend and a singluar frontend, but I wanted a monorepo for the AppType that Hono exposes to allow for route typesafety for my backend API.
So this one will be quick, lets get a monorepo setup using Bun as our package manager, get a Hono backend scaffolded, a Vite + React frontend started, and have that frontend depend on the backend for the Hono AppType.
Easy starting off, lets make a directory for our project and initialize an empty git repo in it.
1mkdir bun-monorepo 2cd bun-monorepo 3git init 4bun init 5 6 7zed . # Optionally open the directory in Zed
If bun init asks for a template I just used the Blank one. This gets our git repository initialized as well as our root package.json and modules folder. First step is to replace the starting package.json to turn our repo into an npm workspace as well as setup our root tsconfig.json. You can also delete the default Bun index.ts
package.json1{ 2 "name": "bun-monorepo", 3 "private": true, 4 "workspaces": [ 5 "packages/*" 6 ], 7 "scripts": { 8 "dev:frontend": "bun --filter @monorepo/frontend dev", 9 "dev:backend": "bun --filter @monorepo/backend dev", 10 "build:frontend": "bun --filter @monorepo/frontend build", 11 "build:backend": "bun --filter @monorepo/backend build" 12 } 13} 14
tsconfig.json1{ 2 "compilerOptions": { 3 "strict": true, 4 "skipLibCheck": true, 5 }, 6 "exclude": ["node_modules"], 7}
Next lets create our backend.
1bun create hono@latest packages/backend
When prompted for the Hono template I chose vercel. Next cd into the backend using cd packages/backend and lets install somethings.
1bun add zod @hono/zod-validator
Then lets edit our backend package.json
packages/backend/package.json1{ 2 "name": "@monorepo/backend", 3 "private": true, 4 "type": "module", 5 "main": "./src/index.ts", 6 "types": "./src/index.ts", 7 "scripts": { 8 "dev": "bun run --hot src/index.ts", 9 "build": "tsc --noEmit" 10 }, 11 "dependencies": { 12 "@hono/zod-validator": "^0.9.1", 13 "hono": "^4.13.9", 14 "zod": "^4.6.5" 15 }, 16 "devDependencies": { 17 "@types/node": "^26.1.1", 18 "tsx": "^4.23.0", 19 "typescript": "^7.0.2" 20 } 21}
Notice the "main", "types", and "scripts" we added and changed. This is so Vercel knows our backend entrypoint, TypeScript knows our types, and we can use some scripts to run the dev server or "build" the backend, basically type-check it. the zod validation lets us validate at runtime while Honos RPC provides the typing to the frontend in a complimentary manner. Next lets change up the backend's tsconfig.json
packages/backend/tsconfig.json1{ 2 "extends": "../../tsconfig.json", 3 "compilerOptions": { 4 "target": "ESNext", 5 "module": "ESNext", 6 "moduleResolution": "Bundler", 7 "strict": true, 8 "paths": { 9 "@/*": ["./src/*"] 10 } 11 }, 12 "exclude": ["node_modules", "dist"] 13}
The "paths" field lets me use the @ import alias when importing from within the backend. So instead of writing import { createUserSchema } from "../schemas/users"; I can just write import { createUserSchema } from "@/schemas/users";
Next lets edit the Hono src/index.ts file to create our simple demo backend with a validated GET and POST request. We also are going to use the hono/cors package to allow our browser frontend to hit the API without any of those pesky CORS errors for different origins.
packages/backend/src/index.ts1import { Hono } from "hono"; 2import { cors } from "hono/cors"; 3import { zValidator } from "@hono/zod-validator"; 4import { z } from "zod"; 5 6const app = new Hono(); 7 8app.use( 9 "/api/*", 10 cors({ 11 origin: "http://localhost:5173", 12 }), 13); 14 15const routes = app 16 .get("/api/hello", (c) => { 17 return c.json({ 18 message: "Hello from Hono!", 19 }); 20 }) 21 .post( 22 "/api/hello", 23 zValidator( 24 "json", 25 z.object({ 26 message: z.string().min(1), 27 }), 28 ), 29 (c) => { 30 const body = c.req.valid("json"); 31 32 return c.json({ 33 message: body.message, 34 }); 35 }, 36 ); 37 38export type AppType = typeof routes; 39 40export default app;
And that awesome AppType export is what will make the type-safe frontend API calls happen. Let's get back to the root of our project with cd ../.. and get started on the frontend.
1bun create vite packages/frontend --template react-ts
That command gets our Vite + React frontend started and then we can run bun install from the root just to make sure all dependencies are installed across the repo. Now lets change the frontend package.json
packages/frontend/package.json1{ 2 "name": "@monorepo/frontend", 3 "private": true, 4 "version": "0.0.0", 5 "type": "module", 6 "scripts": { 7 "dev": "vite", 8 "build": "tsc -b && vite build", 9 "lint": "eslint .", 10 "preview": "vite preview" 11 }, 12 "dependencies": { 13 "@monorepo/backend": "workspace:*", 14 "react": "^19.2.8", 15 "react-dom": "^19.2.8" 16 }, 17 "devDependencies": { 18 "@eslint/js": "^10.0.1", 19 "@types/node": "^24.13.3", 20 "@types/react": "^19.2.18", 21 "@types/react-dom": "^19.2.7", 22 "@vitejs/plugin-react": "^6.1.1", 23 "eslint": "^10.10.0", 24 "eslint-plugin-react-hooks": "^7.1.1", 25 "eslint-plugin-react-refresh": "^0.5.6", 26 "globals": "^17.12.0", 27 "typescript": "~6.0.2", 28 "typescript-eslint": "^8.69.0", 29 "vite": "^8.3.0" 30 } 31}
Notice we changed the name and added our @monorepo/backend package as a dependency. Now from the root of our project we can run bun add hono --filter @monorepo/frontend and Bun will install the hono package on our frontend so we can use the API client. Vite generates 3 different tsconfig.json files so in the app and node one I am just going to have them extend my project root config by adding "extends": "../../tsconfig.json", before "compilerOptions but otherwise leave them untouched.
Now to create the frontend's API client. In the frontend package under the src directory I'll create lib/api.ts.
packages/frontend/src/lib/api.ts1import { hc } from "hono/client"; 2import type { AppType } from "@monorepo/backend"; 3 4export const client = hc<AppType>("http://localhost:3000");
Now in our App.tsx lets make a stupid simple frontend to test our type-safe API from Hono
packages/frontend/src/App.tsx1import { useState } from "react"; 2import { client } from "./lib/api"; 3 4function App() { 5 const [getMessage, setGetMessage] = useState(""); 6 const [postMessage, setPostMessage] = useState(""); 7 8 async function testGet() { 9 const response = await client.api.hello.$get(); 10 11 if (!response.ok) { 12 throw new Error("GET request failed"); 13 } 14 15 const data = await response.json(); 16 17 setGetMessage(data.message); 18 } 19 20 async function testPost() { 21 const response = await client.api.hello.$post({ 22 json: { 23 message: "Hello from React!", 24 }, 25 }); 26 27 if (!response.ok) { 28 throw new Error("POST request failed"); 29 } 30 31 const data = await response.json(); 32 33 setPostMessage(data.message); 34 } 35 36 return ( 37 <main> 38 <h1>Hono API Test</h1> 39 40 <section> 41 <h2>GET /api/hello</h2> 42 43 <button onClick={testGet}> 44 Test GET 45 </button> 46 47 <p>{getMessage}</p> 48 </section> 49 50 <section> 51 <h2>POST /api/hello</h2> 52 53 <button onClick={testPost}> 54 Test POST 55 </button> 56 57 <p>{postMessage}</p> 58 </section> 59 </main> 60 ); 61} 62 63export default App;
Save and let's test it.
From the root, in two terminals I'll run bun run dev:backend and bun run dev:frontend. If we open our frontend URL in the browser and click the buttons to test the route's various HTTP methods we should get repsonses! Yay. We can finally start working on the project...
Thanks for coming to my TED talk.