Building a Full-Stack Application with Hono, Cloudflare Workers, R2 Object Storage, and D1 Database

72 min read

In this article, we will walk through how to build a complete full-stack application using some of Cloudflare’s latest serverless technologies—Hono as the web framework, Workers for serverless computing, R2 for object storage, and D1 for the SQL database. This technology stack allows you to build and deploy highly scalable applications on Cloudflare's edge network.

Technology Overview

Here’s a brief introduction to the key technologies we’ll be using:

  • Hono: A lightweight, simple, and fast web framework designed for Cloudflare Workers. Hono provides an Express.js-like middleware and routing layer.

  • Cloudflare Workers: Serverless functions that run on Cloudflare’s edge network, allowing you to build applications without managing infrastructure.

  • R2 Storage: S3-compatible object storage running at the edge, seamlessly integrated with Workers.

  • D1 Database: A SQL database (based on SQLite) running at Cloudflare’s edge alongside your Worker.

By combining these technologies, you can build globally distributed, fast, and automatically scalable applications that can handle any load—using familiar paradigms like HTTP middleware and SQL.

Setting Up the Project

First, ensure you have the latest Wrangler CLI installed for local development of Worker projects:

npm install -g wrangler 

Then, initialize a new Worker project with Hono:

npm init hono .

This will set up a new Hono project in the current directory. Now we can add the necessary additional dependencies:

npm install hono nanoid @cloudflare/workers-types

This installs the Hono framework, nanoid for generating unique IDs, and types for Workers development.

Next, authenticate the Wrangler CLI with your Cloudflare account and configure your account details, routes, D1 database bindings, and R2 bucket bindings in the project’s wrangler.toml file.

Configuring Bindings

Define bindings in wrangler.toml to tell Wrangler and the Workers runtime how to map variables used in the code. For this example, we’ll configure the R2 bucket, username, and password:

name = "my-app"
type = "javascript"

account_id = "<YOUR_ACCOUNT_ID>"
workers_dev = true

[[r2_buckets]]
binding = "MY_BUCKET" 
bucket_name = "<YOUR_BUCKET_NAME>"

[[d1_databases]]
binding = "DB"
database_name = "my_db"
database_id = "<YOUR_DB_ID>"

[vars]
USERNAME = "<YOUR_USERNAME>"  
PASSWORD = "<YOUR_PASSWORD>"

# Other configurations...

After filling in these bindings with the appropriate credentials, Wrangler will set these environment variables each time you deploy, making them available in your code under the c.env object.

Writing Server-Side Code

Now let’s implement the server-side application in src/index.ts:

import { Hono } from 'hono'
import { bearerAuth } from "hono/bearer-auth";
import { logger } from 'hono/logger' 
import { nanoid } from "nanoid";

type Bindings = {
    MY_BUCKET: R2Bucket
    USERNAME: string
    PASSWORD: string  
}

const app = new Hono<{ Bindings: Bindings }>()

app.use(logger())
app.use('/api/*', bearerAuth({token: Env.TOKEN}))

app.get('/', (c) => {    
    return c.json({ ok: true })
})

app.post('/api/v1/upload', async (c) => {
    const key = nanoid(10)
    const formData = await c.req.parseBody()
    const file = formData['file']
    
    if (file instanceof File) {
        const fileBuffer = await file.arrayBuffer()
        const ext = file.name.split('.').pop()
        const path = `images/${key}.${ext}`
        await c.env.MY_BUCKET.put(path, fileBuffer)    
        return c.json({ 'image': { 'url': `${Env.HOST}${path}` }})
    } else {
        return c.text('Invalid file', 400)
    }
})

export default app

Here we set up several routes:

  • GET /: A health check endpoint.
  • POST /api/v1/upload: An authenticated endpoint for uploading images to R2.

The /upload endpoint expects a multipart/form-data body with a file field containing the image to be uploaded. It uses nanoid to generate a unique key, uploads the file to the R2 bucket, and returns a public URL.

Deploying to Cloudflare

Once the code is ready, we can test it locally using Wrangler:

wrangler dev

This will start a local server that simulates the Workers runtime.

When you're ready to deploy the application to your Cloudflare account:

wrangler publish