create-bhaze
v1.0.7
Published
<div align="center">
Readme
⚡ bhaze
Bun · Hono · Arc · Zod · Ecosystem
A fast, opinionated, production-ready backend starter kit.
Zero bloat. Strict types. Clean architecture.
Table of Contents
- Stack
- Project Structure
- Getting Started
- Environment Variables
- Path Aliases
- API Architecture
- Response Shape
- Scripts
- Adding a New Resource
- Code Style
- TypeScript Config
- Production Deployment
Stack
| Layer | Tool | Version | Purpose |
| ---------- | ------------------- | -------- | ----------------------------------- |
| Runtime | Bun | ^1.x | JS runtime, package manager, runner |
| Framework | Hono | ^4.7 | Ultrafast web framework |
| ORM | Prisma | ^6.0 | Type-safe DB access |
| Validation | Zod | ^3.24 | Schema validation + type inference |
| Middleware | @hono/zod-validator | ^0.4 | Route-level Zod integration |
| Language | TypeScript | latest | Full strict mode |
| Formatter | Prettier | latest | Opinionated code formatting |
Project Structure
bhaze/
├── prisma/
│ └── schema.prisma # Database schema
│
├── src/
│ ├── index.ts # App entry point — Hono init, middleware, server export
│ │
│ ├── config/
│ │ └── env.ts # Zod-validated env variables — crashes early if invalid
│ │
│ ├── routes/
│ │ ├── index.ts # Root router — registers all sub-routers under /api
│ │ └── user.routes.ts # User resource routes with zValidator middleware
│ │
│ ├── controllers/
│ │ └── user.controller.ts # HTTP layer only — reads request, returns response
│ │
│ ├── services/
│ │ └── user.service.ts # Business logic + all Prisma DB calls
│ │
│ ├── middlewares/
│ │ └── error.middleware.ts # Global error handler — catches Zod + generic errors
│ │
│ ├── validators/
│ │ └── user.validator.ts # Zod schemas + inferred TypeScript types
│ │
│ ├── lib/
│ │ ├── db.ts # Prisma singleton with environment-aware logging
│ │ └── response.ts # Typed response helpers: ok, created, fail, notFound…
│ │
│ └── types/ # Shared TypeScript interfaces and utility types
│
├── .env.example # Required env variables template
├── .prettierrc # Prettier formatting rules
├── .prettierignore # Files excluded from formatting
├── bunfig.toml # Bun runtime config + path alias resolution
├── tsconfig.json # Strict TypeScript config
└── package.jsonGetting Started
Prerequisites
- Bun
>= 1.0—curl -fsSL https://bun.sh/install | bash - A running PostgreSQL instance (or swap to SQLite for local dev)
Installation
# 1. Clone or use as template
git clone https://github.com/yourname/bhaze.git
cd bhaze
# 2. Install dependencies
bun install
# 3. Set up environment
cp .env.example .env
# → Edit .env and fill in DATABASE_URL
# 4. Generate Prisma client
bun db:generate
# 5. Run migrations
bun db:migrate
# 6. Start development server
bun devServer starts at http://localhost:3000.
Health check: GET http://localhost:3000/health
Environment Variables
Defined and validated in src/config/env.ts using Zod.
The app will not start if any required variable is missing or malformed — fail fast, not silently.
# .env.example
PORT=3000
DATABASE_URL="postgresql://user:password@localhost:5432/bhaze_db"
NODE_ENV=development # development | production | test| Variable | Required | Default | Description |
| -------------- | -------- | ------------- | --------------------------- |
| DATABASE_URL | ✅ Yes | — | Prisma DB connection string |
| PORT | ❌ No | 3000 | HTTP server port |
| NODE_ENV | ❌ No | development | Runtime environment |
Never commit
.envto version control. Only.env.exampleis tracked.
Path Aliases
Configured in both tsconfig.json (TypeScript resolution) and bunfig.toml (Bun runtime resolution). Both are required — tsconfig alone only satisfies the type checker, not the actual runtime.
| Alias | Resolves to |
| ---------------- | ------------------- |
| @/* | src/* |
| @routes/* | src/routes/* |
| @controllers/* | src/controllers/* |
| @services/* | src/services/* |
| @middlewares/* | src/middlewares/* |
| @validators/* | src/validators/* |
| @lib/* | src/lib/* |
| @types/* | src/types/* |
| @config/* | src/config/* |
// ✅ Use this
import { UserService } from "@services/user.service";
import { ok, notFound } from "@lib/response";
// ❌ Not this
import { UserService } from "../../services/user.service";API Architecture
Request flow is strictly one-directional. No layer skips another.
HTTP Request
│
▼
Route → zValidator middleware validates body/params against Zod schema
│
▼
Controller → Reads validated request, calls service, returns response helper
│
▼
Service → All business logic and Prisma DB calls live here
│
▼
Prisma (DB)Rules:
- Controllers never touch
dbdirectly - Services never touch
c(Hono context) or HTTP concerns - Validators define the schema — types are inferred from them, never written by hand
- Response shape always goes through
src/lib/response.ts
Response Shape
Every API response follows a consistent envelope. Never return raw data directly.
Success
{
"success": true,
"message": "User created",
"data": { "id": "clx...", "name": "Afif", "email": "[email protected]" }
}Error
{
"success": false,
"message": "User not found"
}Validation Error (422)
{
"success": false,
"message": "Validation error",
"errors": {
"email": ["Invalid email"]
}
}Available Response Helpers (src/lib/response.ts)
ok(c, data) // 200
created(c, data, message?) // 201
fail(c, message, status?) // 400 (default)
notFound(c, message?) // 404
unauthorized(c, message?) // 401
forbidden(c, message?) // 403Scripts
bun dev # Start dev server with hot reload (--watch)
bun start # Start production server
bun db:generate # Generate Prisma client after schema changes
bun db:migrate # Create and apply a new migration (dev)
bun db:push # Push schema to DB without migration (prototyping)
bun db:studio # Open Prisma Studio GUIAdding a New Resource
Follow this sequence every time — no exceptions, no shortcuts.
# Example: adding a Product resource1. Validator — src/validators/product.validator.ts
import { z } from "zod";
export const createProductSchema = z.object({
name: z.string().min(1),
price: z.number().positive(),
sku: z.string().min(1),
});
export const updateProductSchema = createProductSchema.partial();
export type CreateProductInput = z.infer<typeof createProductSchema>;
export type UpdateProductInput = z.infer<typeof updateProductSchema>;2. Service — src/services/product.service.ts
import { db } from "@lib/db";
import type {
CreateProductInput,
UpdateProductInput,
} from "@validators/product.validator";
export const ProductService = {
getAll: () => db.product.findMany(),
getById: (id: string) => db.product.findUnique({ where: { id } }),
create: (data: CreateProductInput) => db.product.create({ data }),
update: (id: string, data: UpdateProductInput) =>
db.product.update({ where: { id }, data }),
remove: (id: string) => db.product.delete({ where: { id } }),
};3. Controller — src/controllers/product.controller.ts
import type { Context } from "hono";
import { ProductService } from "@services/product.service";
import { ok, created, notFound } from "@lib/response";
import type {
CreateProductInput,
UpdateProductInput,
} from "@validators/product.validator";
export const ProductController = {
async getAll(c: Context) {
return ok(c, await ProductService.getAll());
},
async getById(c: Context) {
const product = await ProductService.getById(c.req.param("id"));
return product ? ok(c, product) : notFound(c, "Product not found");
},
async create(c: Context) {
const body = c.req.valid("json") as CreateProductInput;
return created(c, await ProductService.create(body), "Product created");
},
async update(c: Context) {
const body = c.req.valid("json") as UpdateProductInput;
return ok(
c,
await ProductService.update(c.req.param("id"), body),
"Product updated",
);
},
async remove(c: Context) {
await ProductService.remove(c.req.param("id"));
return ok(c, null, "Product deleted");
},
};4. Route — src/routes/product.routes.ts
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { ProductController } from "@controllers/product.controller";
import {
createProductSchema,
updateProductSchema,
} from "@validators/product.validator";
export const productRoutes = new Hono();
productRoutes.get("/", ProductController.getAll);
productRoutes.get("/:id", ProductController.getById);
productRoutes.post(
"/",
zValidator("json", createProductSchema),
ProductController.create,
);
productRoutes.patch(
"/:id",
zValidator("json", updateProductSchema),
ProductController.update,
);
productRoutes.delete("/:id", ProductController.remove);5. Register — src/routes/index.ts
import { productRoutes } from "@routes/product.routes";
appRoutes.route("/products", productRoutes);6. Schema — prisma/schema.prisma
model Product {
id String @id @default(cuid())
name String
price Float
sku String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}bun db:migrate # then give the migration a nameDone. New endpoint live at /api/products.
Code Style
Formatted with Prettier. Config in .prettierrc.
# Format all files
bunx prettier --write .
# Check without writing
bunx prettier --check .Key rules:
| Rule | Value |
| --------------- | ------ |
| Semicolons | true |
| Quotes | Double |
| Trailing commas | All |
| Print width | 100 |
| Tab width | 2 |
| Arrow parens | Always |
| End of line | lf |
TypeScript Config
Full strict mode — every safety flag is explicitly enabled, not inherited silently from "strict": true.
Key flags beyond the default strict umbrella:
| Flag | Effect |
| ------------------------------------ | -------------------------------------------------- |
| noUncheckedIndexedAccess | arr[i] returns T \| undefined — must handle |
| exactOptionalPropertyTypes | Can't set optional props to undefined explicitly |
| noPropertyAccessFromIndexSignature | Must use bracket notation for index signatures |
| noImplicitReturns | Every code path must return |
| verbatimModuleSyntax | import type enforced — no runtime leaks |
| isolatedModules | Each file transpilable independently (Bun req) |
| noEmit | Bun handles transpilation — TS is type-check only |
Run type checking:
bunx tsc --noEmitProduction Deployment
Build & Run
Bun runs TypeScript directly — no compile step needed.
NODE_ENV=production bun startDocker
FROM oven/bun:1 AS base
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile --production
COPY . .
RUN bunx prisma generate
EXPOSE 3000
ENV NODE_ENV=production
CMD ["bun", "src/index.ts"]docker build -t bhaze .
docker run -p 3000:3000 --env-file .env bhazePre-deploy Checklist
- [ ]
DATABASE_URLset in production environment - [ ]
NODE_ENV=productionset - [ ]
bun db:migraterun against production DB (notdb:push) - [ ]
bunx prisma generaterun after deploy if schema changed - [ ] Health check endpoint verified:
GET /health - [ ] No
.envfile committed to version control
Built with ⚡ by bhaze
