Hono API Layer

Lightweight REST API backend for authentication, database proxy, and file storage.

What is Hono?

Hono is an ultrafast, lightweight web framework. AppLighter uses Hono in apps/server/ for backend operations.

Server Structure

apps/server/
├── src/
│   ├── index.ts          # Server entry point
│   ├── db/
│   │   ├── schema.ts     # Database schema
│   │   └── store.ts      # In-memory/persistent store
│   ├── middleware/
│   │   └── auth.ts       # JWT authentication
│   └── routes/
│       ├── auth.ts       # Auth endpoints
│       ├── db.ts         # Database proxy
│       ├── posts.ts      # Posts CRUD
│       ├── profiles.ts   # User profiles
│       └── storage.ts    # File storage
├── package.json
├── tsconfig.json
└── .env.example

Use Cases

  • Authentication - JWT-based sign up, sign in, sign out
  • Database Proxy - REST API for Vibecode DB custom adapter
  • File Storage - Upload and serve files
  • Webhooks - Handle external service callbacks
  • Server Actions - Operations requiring secrets

Server Entry Point

// src/index.ts
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { authRoutes } from './routes/auth'
import { dbRoutes } from './routes/db'
import { postsRoutes } from './routes/posts'
import { profilesRoutes } from './routes/profiles'
import { storageRoutes } from './routes/storage'

const app = new Hono()

// Middleware
app.use('*', cors())

// Health check
app.get('/health', (c) => c.json({ status: 'ok' }))

// Mount routes
app.route('/auth', authRoutes)
app.route('/db', dbRoutes)
app.route('/posts', postsRoutes)
app.route('/profiles', profilesRoutes)
app.route('/storage', storageRoutes)

export default {
  port: process.env.PORT || 3001,
  fetch: app.fetch,
}

Auth Routes

// src/routes/auth.ts
import { Hono } from 'hono'
import { sign, verify } from 'hono/jwt'
import { v4 as uuid } from 'uuid'

const auth = new Hono()

auth.post('/signup', async (c) => {
  const { email, password, name } = await c.req.json()

  // Create user
  const user = {
    id: uuid(),
    email,
    name,
    created_at: new Date().toISOString(),
  }

  // Store user (implement your storage)
  await store.users.create(user, password)

  // Generate JWT
  const token = await sign(
    { sub: user.id, email: user.email },
    process.env.JWT_SECRET!
  )

  return c.json({
    user,
    session: { access_token: token },
  })
})

auth.post('/signin', async (c) => {
  const { email, password } = await c.req.json()

  // Verify credentials
  const user = await store.users.verify(email, password)
  if (!user) {
    return c.json({ error: 'Invalid credentials' }, 401)
  }

  // Generate JWT
  const token = await sign(
    { sub: user.id, email: user.email },
    process.env.JWT_SECRET!
  )

  return c.json({
    user,
    session: { access_token: token },
  })
})

auth.post('/signout', (c) => {
  // Client handles token removal
  return c.json({ success: true })
})

auth.get('/session', async (c) => {
  const authHeader = c.req.header('Authorization')
  if (!authHeader) {
    return c.json({ user: null, session: null })
  }

  const token = authHeader.replace('Bearer ', '')

  try {
    const payload = await verify(token, process.env.JWT_SECRET!)
    const user = await store.users.findById(payload.sub as string)
    return c.json({ user, session: { access_token: token } })
  } catch {
    return c.json({ user: null, session: null })
  }
})

export { auth as authRoutes }

Auth Middleware

// src/middleware/auth.ts
import { createMiddleware } from 'hono/factory'
import { verify } from 'hono/jwt'

export const authMiddleware = createMiddleware(async (c, next) => {
  const authHeader = c.req.header('Authorization')

  if (!authHeader) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const token = authHeader.replace('Bearer ', '')

  try {
    const payload = await verify(token, process.env.JWT_SECRET!)
    c.set('userId', payload.sub)
    c.set('userEmail', payload.email)
    await next()
  } catch {
    return c.json({ error: 'Invalid token' }, 401)
  }
})

Database Proxy Routes

For the custom adapter to communicate with the server:

// src/routes/db.ts
import { Hono } from 'hono'
import { authMiddleware } from '../middleware/auth'

const db = new Hono()

// Apply auth to all routes
db.use('*', authMiddleware)

// Generic query endpoint
db.post('/query', async (c) => {
  const { table, operation, filters, data, options } = await c.req.json()
  const userId = c.get('userId')

  // Execute query based on operation
  switch (operation) {
    case 'select':
      const results = await store[table].find(filters, options)
      return c.json({ data: results, error: null })

    case 'insert':
      await store[table].create({ ...data, user_id: userId })
      return c.json({ data: null, error: null })

    case 'update':
      await store[table].update(filters, data)
      return c.json({ data: null, error: null })

    case 'delete':
      await store[table].delete(filters)
      return c.json({ data: null, error: null })

    default:
      return c.json({ data: null, error: 'Unknown operation' }, 400)
  }
})

export { db as dbRoutes }

Storage Routes

// src/routes/storage.ts
import { Hono } from 'hono'
import { authMiddleware } from '../middleware/auth'

const storage = new Hono()

storage.use('*', authMiddleware)

storage.post('/upload', async (c) => {
  const formData = await c.req.formData()
  const file = formData.get('file') as File

  if (!file) {
    return c.json({ error: 'No file provided' }, 400)
  }

  // Upload to S3 or local storage
  const url = await uploadFile(file)

  return c.json({ url })
})

storage.get('/:filename', async (c) => {
  const filename = c.req.param('filename')
  const file = await getFile(filename)

  if (!file) {
    return c.notFound()
  }

  return new Response(file.data, {
    headers: { 'Content-Type': file.contentType },
  })
})

export { storage as storageRoutes }

Environment Variables

# Server port
PORT=3001

# JWT secret (min 32 characters)
JWT_SECRET=your-secret-key-min-32-characters

# Database (optional for production)
DATABASE_URL=postgresql://user:password@localhost:5432/dbname

# S3 Storage (optional)
S3_BUCKET=your-bucket
S3_REGION=us-east-1
S3_ACCESS_KEY=your-access-key
S3_SECRET_KEY=your-secret-key

Running the Server

# Development
pnpm dev:server

# Or directly
cd apps/server && bun run src/index.ts

The server runs on http://localhost:3001 by default.

Connecting from Mobile

Configure the custom adapter to point to your server:

# apps/mobile/.env
EXPO_PUBLIC_ADAPTER=custom
EXPO_PUBLIC_CUSTOM_API_URL=http://localhost:3001

Deployment

Hono runs anywhere - Bun, Node.js, Cloudflare Workers, Vercel:

# Build for Node.js
cd apps/server && bun build src/index.ts --outdir=dist --target=node

# Deploy to your platform of choice

Next Steps