Deploy Bun + Hono REST API to Vercel for Free

Bun is the fastest JavaScript runtime. Hono is the lightest web framework. Vercel gives you serverless deployments with zero configuration. Together, they let you ship a production-ready REST API in under 30 minutes — for free.
This guide walks you through every step: create the project, write the API, push to GitHub, and deploy to Vercel with automatic CI/CD on every git push.
What You'll Learn
✅ Set up a Bun + Hono project from scratch
✅ Build a typed REST API with TypeScript
✅ Configure the Hono Vercel adapter
✅ Push code to a free GitHub public repo
✅ Deploy to Vercel with zero-config CI/CD
✅ Add environment variables and test in production
✅ Understand Vercel's free tier limits for APIs
Prerequisites
Before starting, make sure you have:
- Bun installed (
curl -fsSL https://bun.sh/install | bash) - Git installed
- A free GitHub account
- A free Vercel account (sign up with GitHub at vercel.com)
# Verify Bun is installed
bun --version # 1.x.x
# Verify Git is installed
git --version # git version 2.x.xPart 1: Create the Hono Project
Step 1: Scaffold with create-hono
Hono provides an official scaffolding tool that sets up everything for Vercel automatically.
bun create hono my-apiYou'll be prompted to choose a template. Select vercel:
? Which template do you want to use?
aws-lambda
bun
cloudflare-pages
cloudflare-workers
deno
fastly
lambda-edge
netlify
nextjs
nodejs
❯ vercel
...Then choose TypeScript when asked about the language.
Navigate into the project:
cd my-api
bun installStep 2: Understand the Project Structure
After scaffolding, your project looks like this:
my-api/
├── api/
│ └── index.ts # Vercel serverless entry point
├── src/
│ └── index.ts # Main Hono app
├── package.json
├── tsconfig.json
└── .gitignoreThe key insight: Vercel expects a file at api/index.ts. The scaffolded project already wires this up for you.
Let's look at what was generated:
// api/index.ts
import { handle } from 'hono/vercel'
import app from '../src/index'
export const config = {
runtime: 'edge',
}
export default handle(app)// src/index.ts
import { Hono } from 'hono'
const app = new Hono().basePath('/api')
app.get('/', (c) => {
return c.json({ message: 'Hello Hono!' })
})
export default appThe handle function from hono/vercel wraps your Hono app into a Vercel-compatible handler. The runtime: 'edge' config tells Vercel to run this on the Edge Runtime (faster cold starts than Node.js).
Part 2: Build the REST API
Let's build a real API — a simple task manager with CRUD endpoints.
Step 3: Create the Task Router
Create a dedicated router file for tasks:
mkdir -p src/routes
touch src/routes/tasks.ts// src/routes/tasks.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
// In-memory store (replace with a real DB in production)
type Task = {
id: string
title: string
done: boolean
createdAt: string
}
const tasks: Task[] = [
{ id: '1', title: 'Learn Bun', done: true, createdAt: new Date().toISOString() },
{ id: '2', title: 'Build with Hono', done: false, createdAt: new Date().toISOString() },
]
const createTaskSchema = z.object({
title: z.string().min(1).max(200),
})
const tasksRouter = new Hono()
// GET /api/tasks - List all tasks
tasksRouter.get('/', (c) => {
return c.json({ tasks, total: tasks.length })
})
// GET /api/tasks/:id - Get a single task
tasksRouter.get('/:id', (c) => {
const task = tasks.find((t) => t.id === c.req.param('id'))
if (!task) {
return c.json({ error: 'Task not found' }, 404)
}
return c.json({ task })
})
// POST /api/tasks - Create a task
tasksRouter.post('/', zValidator('json', createTaskSchema), (c) => {
const { title } = c.req.valid('json')
const newTask: Task = {
id: String(Date.now()),
title,
done: false,
createdAt: new Date().toISOString(),
}
tasks.push(newTask)
return c.json({ task: newTask }, 201)
})
// PATCH /api/tasks/:id - Toggle done status
tasksRouter.patch('/:id', (c) => {
const task = tasks.find((t) => t.id === c.req.param('id'))
if (!task) {
return c.json({ error: 'Task not found' }, 404)
}
task.done = !task.done
return c.json({ task })
})
// DELETE /api/tasks/:id - Delete a task
tasksRouter.delete('/:id', (c) => {
const index = tasks.findIndex((t) => t.id === c.req.param('id'))
if (index === -1) {
return c.json({ error: 'Task not found' }, 404)
}
const [deleted] = tasks.splice(index, 1)
return c.json({ task: deleted })
})
export default tasksRouterInstall the Zod validator middleware:
bun add @hono/zod-validator zodStep 4: Update the Main App
Wire the tasks router into the main app:
// src/index.ts
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import tasksRouter from './routes/tasks'
const app = new Hono().basePath('/api')
// Middlewares
app.use('*', logger())
app.use('*', cors())
// Routes
app.route('/tasks', tasksRouter)
// Health check
app.get('/health', (c) => {
return c.json({
status: 'ok',
runtime: 'edge',
timestamp: new Date().toISOString(),
})
})
// 404 fallback
app.notFound((c) => {
return c.json({ error: 'Not found' }, 404)
})
export default appStep 5: Test Locally
bun run devThe development server starts on http://localhost:3000. Test your endpoints:
# Health check
curl http://localhost:3000/api/health
# List tasks
curl http://localhost:3000/api/tasks
# Create a task
curl -X POST http://localhost:3000/api/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Deploy to Vercel"}'
# Get a task
curl http://localhost:3000/api/tasks/1
# Toggle done
curl -X PATCH http://localhost:3000/api/tasks/1
# Delete a task
curl -X DELETE http://localhost:3000/api/tasks/1Expected output for the health check:
{
"status": "ok",
"runtime": "edge",
"timestamp": "2026-02-11T10:00:00.000Z"
}Part 3: Environment Variables
Real APIs need secrets — API keys, database URLs, etc. Hono on Vercel accesses them the standard way.
Step 6: Add Environment Variable Support
// src/index.ts (updated)
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import tasksRouter from './routes/tasks'
// Type your env vars for full type safety
type Env = {
Bindings: {
DATABASE_URL: string
API_SECRET: string
}
}
const app = new Hono<Env>().basePath('/api')
app.use('*', logger())
app.use('*', cors())
app.route('/tasks', tasksRouter)
app.get('/health', (c) => {
// Access env vars via c.env (Edge Runtime) or process.env (Node.js)
const hasDb = !!c.env?.DATABASE_URL || !!process.env.DATABASE_URL
return c.json({
status: 'ok',
database: hasDb ? 'connected' : 'not configured',
timestamp: new Date().toISOString(),
})
})
app.notFound((c) => c.json({ error: 'Not found' }, 404))
export default appCreate a .env.local file for local development:
# .env.local
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
API_SECRET=my-local-secretAdd it to .gitignore — never commit secrets to Git:
# .gitignore
node_modules/
.env
.env.local
.env*.local
.vercelPart 4: Push to GitHub
Step 7: Create a GitHub Repository
- Go to github.com/new
- Set Repository name to
my-api - Set visibility to Public (required for Vercel free tier with unlimited deployments)
- Do NOT initialize with README (we already have our code)
- Click Create repository
Step 8: Initialize Git and Push
Back in your terminal:
# Initialize git repo
git init
# Stage all files
git add .
# First commit
git commit -m "feat: init Hono API with Vercel adapter"
# Add GitHub remote (replace YOUR_USERNAME with your GitHub username)
git remote add origin https://github.com/YOUR_USERNAME/my-api.git
# Push to GitHub
git branch -M main
git push -u origin mainVerify your code is on GitHub by visiting https://github.com/YOUR_USERNAME/my-api.
Part 5: Deploy to Vercel
Step 9: Import Project to Vercel
- Go to vercel.com/new
- Click "Import Git Repository"
- Connect your GitHub account if not already connected
- Find
my-apiand click Import
Vercel will auto-detect it's a Hono project and configure the build settings automatically. You should see:
- Framework Preset: Other
- Root Directory:
./ - Build Command: (left empty or
bun run build) - Output Directory: (left empty)
- Install Command:
bun install
Step 10: Add Environment Variables in Vercel
Before clicking Deploy, add your environment variables:
- Expand the "Environment Variables" section
- Add each variable:
| Name | Value | Environment |
|---|---|---|
DATABASE_URL | your-db-url | Production, Preview, Development |
API_SECRET | your-secret | Production, Preview, Development |
- Click Deploy
Vercel will build and deploy your API. In about 30-60 seconds, you'll see:
✓ Build completed
✓ Deployment complete
🚀 https://my-api-yourusername.vercel.appStep 11: Test the Production API
Test your live endpoints with the production URL:
BASE_URL="https://my-api-yourusername.vercel.app"
# Health check
curl $BASE_URL/api/health
# List tasks
curl $BASE_URL/api/tasks
# Create a task
curl -X POST $BASE_URL/api/tasks \
-H "Content-Type: application/json" \
-d '{"title": "First production task"}'Part 6: Automatic CI/CD
This is where Vercel shines. Every git push triggers a new deployment automatically.
How it Works
Step 12: Make a Change and See CI/CD in Action
Update the health check response:
// src/index.ts
app.get('/health', (c) => {
return c.json({
status: 'ok',
version: '1.1.0', // Add this
runtime: 'edge',
timestamp: new Date().toISOString(),
})
})Push the change:
git add .
git commit -m "feat: add version to health check"
git pushGo to your Vercel dashboard — you'll see a new deployment building in real time. In ~30 seconds, your change is live.
Preview Deployments for PRs
When you open a pull request on GitHub, Vercel automatically creates a preview deployment with a unique URL like https://my-api-git-feature-branch-yourusername.vercel.app. This lets you test changes before merging to main.
Part 7: Project Configuration Deep Dive
Understanding vercel.json
For most Hono projects the scaffolder handles this, but you can customize the behavior with a vercel.json file:
{
"rewrites": [
{
"source": "/api/(.*)",
"destination": "/api/index"
}
]
}This ensures all /api/* routes are handled by your single Hono entry point.
Edge Runtime vs Node.js Runtime
The runtime: 'edge' in api/index.ts is important:
| Edge Runtime | Node.js Runtime | |
|---|---|---|
| Cold start | ~0ms | 100-500ms |
| Max execution | 30 seconds | 60 seconds |
| Memory | 128MB | 1024MB |
| Node.js APIs | ❌ (Web APIs only) | ✅ |
| File system | ❌ | ❌ (read-only) |
| npm packages | Limited | Full support |
Use Edge Runtime for: simple CRUD APIs, proxy endpoints, auth middleware, lightweight transformations.
Use Node.js Runtime for: heavy computation, packages that need native Node.js APIs, PDF generation, image processing.
To switch to Node.js runtime, update api/index.ts:
// api/index.ts — Node.js runtime variant
import { handle } from 'hono/vercel'
import app from '../src/index'
export const config = {
runtime: 'nodejs', // Changed from 'edge'
maxDuration: 60, // Up to 60s on free tier
}
export default handle(app)Final package.json
Your complete package.json should look like this:
{
"name": "my-api",
"version": "1.0.0",
"scripts": {
"dev": "bun run --hot src/index.ts",
"build": "bun build ./api/index.ts --outdir ./dist",
"start": "bun run dist/index.js"
},
"dependencies": {
"@hono/zod-validator": "^0.4.1",
"hono": "^4.6.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/bun": "latest",
"typescript": "^5.0.0"
}
}Part 8: Vercel Free Tier — What You Get
Understanding the limits helps you plan:
| Feature | Free (Hobby) Tier |
|---|---|
| Deployments | Unlimited |
| Serverless Functions | 100GB-hours / month |
| Edge Function invocations | 500,000 / month |
| Bandwidth | 100GB / month |
| Custom domains | Unlimited |
| SSL certificates | Automatic (free) |
| Preview deployments | ✅ On every PR |
| CI/CD via GitHub | ✅ Automatic |
| Team members | 1 (personal) |
For a REST API serving a side project or small SaaS, the free tier is more than enough — 500,000 edge invocations per month is roughly 16,000 requests per day.
Complete Project Structure
Here's the final structure of your deployed API:
my-api/
├── api/
│ └── index.ts # Vercel entry point (edge runtime)
├── src/
│ ├── index.ts # Hono app + middleware
│ └── routes/
│ └── tasks.ts # Task CRUD router
├── .env.local # Local secrets (git-ignored)
├── .gitignore
├── package.json
├── tsconfig.json
└── vercel.json # Optional Vercel configTroubleshooting
"Function not found" on Vercel
Cause: Vercel can't find your serverless function entry point.
Fix: Make sure api/index.ts exists and exports a default handler:
export default handle(app)CORS errors in the browser
Cause: The cors() middleware isn't applied.
Fix: Add app.use('*', cors()) before your routes in src/index.ts. For production, restrict origins:
app.use('*', cors({
origin: ['https://yourfrontend.com', 'http://localhost:5173'],
}))Environment variables are undefined in production
Cause: Variables added locally but not in Vercel dashboard.
Fix: Go to Vercel dashboard → Your Project → Settings → Environment Variables. Add each variable and redeploy.
Build fails with TypeScript errors
Cause: Strict TypeScript config rejecting implicit any.
Fix: Check tsconfig.json and ensure types are installed:
bun add -d @types/bunSummary and Key Takeaways
You now have a fully deployed REST API with automatic CI/CD — entirely for free.
✅ Hono + Vercel adapter handles the serverless function wiring
✅ Edge Runtime gives near-zero cold starts
✅ GitHub public repo triggers automatic deploys on every push
✅ Vercel free tier supports 500K edge invocations/month
✅ Environment variables keep secrets out of your Git repo
✅ Preview deployments on every PR for safe testing
✅ Zod validation adds type-safe request body parsing
What to Build Next
- Connect a real database: Neon (serverless PostgreSQL, free tier) or Upstash (Redis, free tier)
- Add JWT authentication with Hono's
hono/jwtmiddleware - Add request rate limiting with Hono's
hono/rate-limiter - Write tests with
bun test - Set up a custom domain in Vercel settings
The Bun + Hono + Vercel stack is one of the fastest ways to ship a backend today. You get TypeScript-first development, extreme performance, and zero-config deployments — all on a generous free tier.
📬 Subscribe to Newsletter
Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.
We respect your privacy. Unsubscribe at any time.
💬 Comments
Sign in to leave a comment
We'll never post without your permission.