Back to blog

Deploy Bun + Hono REST API to Vercel for Free

bunhonoverceltypescriptbackendrest-api
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.x

Part 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-api

You'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 install

Step 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
└── .gitignore

The 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 app

The 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 tasksRouter

Install the Zod validator middleware:

bun add @hono/zod-validator zod

Step 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 app

Step 5: Test Locally

bun run dev

The 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/1

Expected 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 app

Create a .env.local file for local development:

# .env.local
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
API_SECRET=my-local-secret

Add it to .gitignorenever commit secrets to Git:

# .gitignore
node_modules/
.env
.env.local
.env*.local
.vercel

Part 4: Push to GitHub

Step 7: Create a GitHub Repository

  1. Go to github.com/new
  2. Set Repository name to my-api
  3. Set visibility to Public (required for Vercel free tier with unlimited deployments)
  4. Do NOT initialize with README (we already have our code)
  5. 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 main

Verify 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

  1. Go to vercel.com/new
  2. Click "Import Git Repository"
  3. Connect your GitHub account if not already connected
  4. Find my-api and 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:

  1. Expand the "Environment Variables" section
  2. Add each variable:
NameValueEnvironment
DATABASE_URLyour-db-urlProduction, Preview, Development
API_SECRETyour-secretProduction, Preview, Development
  1. 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.app

Step 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 push

Go 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 RuntimeNode.js Runtime
Cold start~0ms100-500ms
Max execution30 seconds60 seconds
Memory128MB1024MB
Node.js APIs❌ (Web APIs only)
File system❌ (read-only)
npm packagesLimitedFull 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:

FeatureFree (Hobby) Tier
DeploymentsUnlimited
Serverless Functions100GB-hours / month
Edge Function invocations500,000 / month
Bandwidth100GB / month
Custom domainsUnlimited
SSL certificatesAutomatic (free)
Preview deployments✅ On every PR
CI/CD via GitHub✅ Automatic
Team members1 (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 config

Troubleshooting

"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/bun

Summary 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/jwt middleware
  • 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.