These integration guides are not official documentation and the Strapi Support Team will not provide assistance with them.
What Is Hono?
Hono is a modern web framework built on Web Standards rather than Node.js-specific APIs. Unlike traditional Node.js frameworks, Hono runs natively on Cloudflare Workers, Deno, Bun, and Node.js without code modifications. The framework uses standard Fetch API and Request/Response objects, making your code portable across runtimes.
The framework's architecture centers on performance: it processes around 390,000 requests per second in standardized benchmarks. This speed is supported by multiple optimized router algorithms in Hono—such as RegExpRouter, TrieRouter, SmartRouter (which automatically selects the best router when multiple are registered), and LinearRouter.
Hono provides first-class TypeScript support with full type inference across the request/response lifecycle. The framework's small core (around 11–12KB minified with the tiny preset) makes it suitable for edge computing, where cold start times can impact user experience, although this benefit is inferred rather than backed by official Hono cold-start benchmarks.
Why Integrate Hono with Strapi
Combining Hono with Strapi creates a distributed architecture where your API consumption layer runs at the edge while content management remains centralized. This pattern solves key problems for full-stack developers building content-driven applications:
- Edge deployment reduces global latency — Deploy Hono on Cloudflare Workers or Vercel Edge to handle authentication, validation, and response transformation near users while keeping Strapi's content operations centralized.
- Runtime-agnostic code maximizes flexibility — Write your integration once and deploy across Bun, Node.js, or Cloudflare Workers with minimal configuration changes. Hono's Web Standards foundation means identical code patterns across all environments.
- TypeScript integration provides compile-time safety — Both frameworks prioritize TypeScript. Strapi 5 supports TypeScript configuration, and Hono offers full type inference throughout middleware chains, enabling end-to-end type safety.
- Composable middleware complements Strapi's architecture — Strapi's backend uses Koa.js patterns, and Hono provides a similar clean middleware system with pre-processing and post-processing phases.
How to Integrate Hono with Strapi
This integration connects Hono's routing engine with Strapi's content APIs through environment-based configuration and typed service layers. You build a lightweight API consumption layer that authenticates to Strapi, transforms responses, and deploys to edge locations.
Set Up Your Strapi Backend
Start with a Strapi 5 project. If you're migrating from v4, note that Strapi 5 introduces flattened API responses: attributes now appear directly at the top level of the data object rather than nested inside data.attributes. This simplification affects how you parse responses in your Hono application.
Configure API tokens in the Strapi admin panel at Settings → API Tokens → Create new API Token. Choose the appropriate permission level.
Store the generated token securely; you use it in your Hono application's environment configuration.
For content requiring user authentication, configure the Users & Permissions. Strapi provides JWT authentication out of the box at /api/auth/local. You authenticate users against Strapi and use the returned JWT for subsequent requests.
Initialize Your Hono Project
Create a new Hono project using your preferred package manager:
npm create hono@latest strapi-api
cd strapi-api
npm installDuring project setup, choose your target runtime by selecting the appropriate template. Hono supports Cloudflare Workers for edge deployment, Node.js for traditional server hosting, or other runtimes like Deno and Bun.
If deploying alongside your Strapi instance, Node.js is the typical choice; for edge deployment of a Strapi API consumption layer, Cloudflare Workers enable global distribution while Strapi remains on traditional infrastructure.
Configure environment variables for your Strapi connection. Create a .env file:
STRAPI_URL=http://localhost:1337
STRAPI_API_TOKEN=your-api-token-hereFor Cloudflare Workers, add these to your wrangler.toml:
[vars]
STRAPI_URL = "https://your-strapi-url.com"
[secrets]
STRAPI_API_TOKEN = "your-api-token-here"The distinction matters: vars are non-sensitive configurations accessible throughout your application, while secrets are sensitive tokens that should be securely managed and never logged or exposed.
According to best practices for secure environment configuration, you store sensitive API tokens and credentials as secrets in your runtime platform (such as Vercel environment secrets, Cloudflare Worker secrets or Cloudflare Secrets Store, or .env files excluded from version control).
Build a Strapi Service Layer
Create a service class that abstracts Strapi API interactions. This pattern separates content-fetching logic from route handlers, making your code testable and allowing you to swap CMS backends without changing routes.
// src/services/strapi.ts
import qs from 'qs'
interface StrapiConfig {
baseUrl: string
token: string
}
export class StrapiService {
private config: StrapiConfig
constructor(config: StrapiConfig) {
this.config = config
}
async fetchContent<T>(
endpoint: string,
options: {
populate?: string | object
filters?: object
sort?: string[]
pagination?: { page: number; pageSize: number }
} = {}
): Promise<T> {
const query = qs.stringify(options, { encodeValuesOnly: true })
const response = await fetch(
`${this.config.baseUrl}/api/${endpoint}?${query}`,
{
headers: {
'Authorization': `Bearer ${this.config.token}`,
'Content-Type': 'application/json'
}
}
)
if (!response.ok) {
throw new Error(`Strapi request failed: ${response.statusText}`)
}
return response.json()
}
async createContent<T>(endpoint: string, data: object): Promise<T> {
const response = await fetch(
`${this.config.baseUrl}/api/${endpoint}`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${this.config.token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ data })
}
)
if (!response.ok) {
throw new Error(`Failed to create content: ${response.statusText}`)
}
return response.json()
}
}The Strapi REST API recommends the qs library for handling complex query string generation when working with Strapi's filtering and population syntax.
Implement REST API Routes
Connect your Hono routes to the Strapi service layer. This example demonstrates fetching articles with pagination and relation population; additional work is needed to implement Strapi-compliant search filtering:
// src/index.ts
import { Hono } from 'hono'
import { StrapiService } from './services/strapi'
type Bindings = {
STRAPI_URL: string
STRAPI_API_TOKEN: string
}
const app = new Hono<{ Bindings: Bindings }>()
// Initialize Strapi service with proper environment configuration
app.use('*', async (c, next) => {
// Create typed Strapi client with environment variables
const strapiClient = {
baseUrl: c.env.STRAPI_URL,
token: c.env.STRAPI_API_TOKEN,
// Helper method for authenticated API requests
async fetch(endpoint: string, options: RequestInit = {}) {
const url = `${this.baseUrl}${endpoint}`
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.token}`,
...options.headers
}
})
if (!response.ok) {
throw new Error(`Strapi API error: ${response.statusText}`)
}
return response.json()
}
}
// Make Strapi client available throughout request lifecycle
c.set('strapi', strapiClient)
await next()
})
// List articles with pagination
app.get('/articles', async (c) => {
const page = Number(c.req.query('page')) || 1
const pageSize = Number(c.req.query('pageSize')) || 10
const search = c.req.query('search')
const strapiUrl = c.env.STRAPI_URL
const apiToken = c.env.STRAPI_API_TOKEN
// Build query object for Strapi REST API
const queryObject = {
pagination: { page, pageSize },
populate: ['author', 'categories'],
sort: ['publishedAt:desc']
}
// Add filtering if search parameter provided
if (search) {
queryObject.filters = {
title: { $containsi: search }
}
}
// Convert to query string using qs library
const qs = require('qs')
const query = qs.stringify(queryObject, { encodeValuesOnly: true })
// Fetch from Strapi REST API
const response = await fetch(`${strapiUrl}/api/articles?${query}`, {
headers: {
'Authorization': `Bearer ${apiToken}`
}
})
if (!response.ok) {
return c.json({ error: 'Failed to fetch articles' }, response.status)
}
const data = await response.json()
return c.json({
articles: data.data,
pagination: data.meta.pagination
})
})
// Get single article by slug
app.get('/articles/:slug', async (c) => {
const slug = c.req.param('slug')
try {
const response = await fetch(`${c.env.STRAPI_URL}/api/articles?filters[slug][$eq]=${slug}&populate=*`, {
headers: {
'Authorization': `Bearer ${c.env.STRAPI_API_TOKEN}`
}
})
if (!response.ok) {
return c.json({ error: 'Article not found' }, response.status)
}
const data = await response.json()
return c.json(data)
} catch (error) {
return c.json({ error: 'Failed to fetch article' }, 500)
}
})
export default appNotice the middleware pattern: in a typical Hono setup, the Strapi service would be initialized per request inside the middleware unless you move the initialization outside the middleware function or cache it yourself, and then attached to the context. Route handlers access it through c.get('strapi') without recreating the service instance.
Add JWT Authentication
For user-specific content or protected operations, implement JWT authentication. This flow authenticates users against Strapi and validates tokens in subsequent requests:
import { jwt } from 'hono/jwt'
// Login endpoint - exchange credentials for JWT
app.post('/auth/login', async (c) => {
const { identifier, password } = await c.req.json()
const response = await fetch(`${c.env.STRAPI_URL}/api/auth/local`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identifier, password })
})
if (!response.ok) {
return c.json({ error: 'Authentication failed' }, 401)
}
const data = await response.json()
return c.json({
jwt: data.jwt,
user: data.user
})
})
// Protected routes require JWT
app.use('/api/*', (c, next) => {
const jwtMiddleware = jwt({
secret: c.env.JWT_SECRET || process.env.JWT_SECRET
})
return jwtMiddleware(c, next)
})// Protected route using JWT middleware
app.use('/api/*', jwt({ secret: JWT_SECRET }))
app.get('/api/me/profile', async (c) => {
const payload = c.get('jwtPayload')
if (!payload || !payload.id) {
return c.json({ error: 'Invalid token' }, 401)
}
const tokenHeader = c.req.header('Authorization') ?? ''
const match = tokenHeader.match(/^Bearer\s+(.+)$/i)
const token = match ? match[1].trim() : undefined
if (!token) {
return c.json({ error: 'Authorization header missing' }, 401)
}
try {
const response = await fetch(
`${c.env.STRAPI_URL}/api/users/${payload.id}`,
{
headers: { 'Authorization': `Bearer ${token}` }
}
)
if (!response.ok) {
return c.json({ error: 'Failed to fetch user profile' }, response.status)
}
return c.json(await response.json())
} catch (error) {
return c.json({ error: 'Failed to fetch profile' }, 500)
}
})The JWT middleware validates tokens before route handlers execute. When applied to protected routes, it verifies tokens and passes the decoded payload to your handler via the context object, allowing you to access user data through c.get('jwtPayload'). You handle invalid or expired tokens by checking for the presence of the payload in your route logic.
Configure GraphQL Integration
If your Strapi instance has the GraphQL plugin installed and enabled, you can query the GraphQL endpoint directly at /graphql or build a GraphQL gateway with Hono using Pylon for improved type safety and code-first GraphQL development:
app.post('/graphql', async (c) => {
const { query, variables } = await c.req.json()
const response = await fetch(`${c.env.STRAPI_URL}/graphql`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${c.env.STRAPI_API_TOKEN}`
},
body: JSON.stringify({ query, variables })
})
return c.json(await response.json())
})This proxy pattern lets your client applications query a single endpoint regardless of whether you're using REST or GraphQL internally. You can implement response caching, query validation, or custom resolvers in the proxy layer.
Deploy to Edge Networks
For Cloudflare Workers deployment, configure your wrangler.toml and deploy:
npm run deployWhen deployed to Cloudflare Workers, your Hono application runs across Cloudflare's edge network globally. Your API routes execute close to users at Cloudflare's edge locations while connecting back to your centrally-hosted Strapi instance.
For Vercel Edge Functions, Hono does not require the @hono/vercel adapter and can run natively with zero configuration.
import { handle } from 'hono/vercel'
export default handle(app)Deploy using the Vercel CLI:
vercel deploy --prodBoth platforms provide zero-configuration deployment: no manual server setup or load balancer configuration required.
Project Example: Edge-Deployed Blog API
This example builds a blog API that runs on Cloudflare Workers, consuming a Strapi 5 backend for content management. The architecture demonstrates how edge deployment reduces latency for read-heavy content sites while keeping content operations centralized.
Architecture Overview
Users request content from Cloudflare's edge network. Your Hono application authenticates requests, fetches from Strapi, transforms responses, and implements caching. Strapi runs on traditional infrastructure (VPS, container platform, or Strapi Cloud) where its database-dependent operations work effectively.
This separation provides two key architectural advantages: your API logic executes at the edge near users globally through runtimes like Cloudflare Workers or Deno Deploy, and you scale the edge API layer independently of your Strapi content management infrastructure without impacting backend performance.
Project Structure
blog-edge-api/
├── src/
│ ├── routes/
│ │ ├── articles.ts
│ │ └── authors.ts
│ ├── services/
│ │ └── strapi.ts
│ ├── middleware/
│ │ └── cache.ts
│ └── index.ts
├── wrangler.toml
└── package.jsonImplement Article Routes
Create a route handler that fetches articles with pagination and filtering:
// src/routes/articles.ts
import { Hono } from 'hono'
import type { StrapiService } from '../services/strapi'
interface Article {
id: number
title: string
slug: string
content: string
publishedAt: string
author: {
name: string
email: string
}
}
const articles = new Hono<{
Variables: { strapi: StrapiService }
}>()
articles.get('/', async (c) => {
const page = Number(c.req.query('page')) || 1
const category = c.req.query('category')
const filters = category ? {
categories: {
slug: { $eq: category }
}
} : {}
const strapi = c.get('strapi')
const response = await strapi.fetchContent<{
data: Article[]
meta: { pagination: object }
}>('articles', {
populate: {
author: { fields: ['name', 'email'] },
categories: { fields: ['name', 'slug'] }
},
filters,
sort: ['publishedAt:desc'],
pagination: { page, pageSize: 20 }
})
return c.json({
articles: response.data,
pagination: response.meta.pagination
})
})
articles.get('/:slug', async (c) => {
const slug = c.req.param('slug')
const strapiUrl = c.env.STRAPI_URL || 'http://localhost:1337'
const apiToken = c.env.STRAPI_API_TOKEN
try {
const response = await fetch(
`${strapiUrl}/api/articles?filters[slug][$eq]=${slug}&populate=*`,
{
headers: {
'Authorization': `Bearer ${apiToken}`
}
}
)
const data = await response.json()
if (!data.data || data.data.length === 0) {
return c.json({ error: 'Article not found' }, 404)
}
return c.json(data.data[0])
} catch (error) {
return c.json({ error: 'Failed to fetch article' }, 500)
}
})
export default articles;The TypeScript generics (fetchContent<T>) ensure type safety from Strapi responses through to your API output. Define your content types once and get autocomplete throughout your route handlers.
Add Edge Caching
Implement response caching using Cloudflare's Cache API. This reduces load on your Strapi backend and improves response times for frequently-accessed content:
// src/middleware/cache.ts
export const cacheMiddleware = async (c, next) => {
const cache = caches.default
const cacheKey = new Request(c.req.url, { method: 'GET' })
// Check cache first
let response = await cache.match(cacheKey)
if (!response) {
// Cache miss - execute route handler
await next()
// Cache successful responses
if (c.res.ok && c.res.status === 200) {
const headers = new Headers(c.res.headers)
headers.set('Cache-Control', 'public, max-age=300') // 5 minutes
const cachedResponse = new Response(c.res.body, {
status: c.res.status,
statusText: c.res.statusText,
headers
})
c.executionCtx.waitUntil(cache.put(cacheKey, cachedResponse))
}
return c.res
}
return response
}
// Apply to article routes
app.get('/articles*', cacheMiddleware)The waitUntil pattern extends the execution lifetime of a request to allow background tasks to complete after the response returns to the user. This is particularly useful in Cloudflare Workers environments where request-response cycles are typically short-lived.
When you enable caching for specific content types, Cloudflare's distributed network serves cached responses from edge locations geographically close to users, improving performance for repeat requests until the cache expires.
Handle Cache Invalidation
When content updates in Strapi, invalidate cached responses using webhooks:
// src/routes/webhooks.ts
import { Hono } from 'hono'
const app = new Hono()
app.post('/webhooks/content-updated', async (c) => {
const signature = c.req.header('X-Webhook-Signature')
const webhookSecret = c.env.WEBHOOK_SECRET
// Verify signature to ensure request came from Strapi
// Implementation depends on your webhook signing strategy
const body = await c.req.json()
// Handle Strapi webhook payload for content updates
if (body.event === 'entry.update' || body.event === 'entry.create') {
const contentType = body.model
if (contentType === 'article') {
const cache = caches.default
// Invalidate cache for the updated content
// Build URL based on the content type and identifier
const url = new URL(`${c.req.url.origin}/articles/${body.entry.slug}`)
await cache.delete(new Request(url.toString()))
}
}
return c.json({ received: true })
})
export default appConfigure webhooks in your Strapi admin panel to call this endpoint when articles are published, updated, or deleted. This ensures users see fresh content while maintaining cache performance for unchanged content.
Complete Application
Wire everything together in your main application file:
// src/index.ts
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { StrapiService } from './services/strapi'
import articles from './routes/articles'
import { cacheMiddleware } from './middleware/cache'
const app = new Hono<{ Bindings: Bindings }>()
// Global middleware
app.use('*', logger())
app.use('*', cors({
origin: 'https://yourdomain.com',
credentials: true,
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']
}))
// Initialize Strapi service
app.use('*', async (c, next) => {
const strapi = new StrapiService({
baseUrl: c.env.STRAPI_URL,
token: c.env.STRAPI_API_TOKEN
})
c.set('strapi', strapi)
await next()
})
// Mount routes
app.route('/articles', articles)
// Apply cache
app.use('/articles/*', cacheMiddleware)
// Health check
app.get('/health', (c) => c.json({ status: 'ok' }))
export default appThis structure keeps your application modular. Routes, services, and middleware remain separate, making the codebase maintainable as you add more content types and features.
Deploy and Test
Deploy to Cloudflare Workers:
npm run deployTest your endpoints:
# Fetch all articles
curl https://your-worker.workers.dev/articles
# Fetch by category
curl https://your-worker.workers.dev/articles?category=technology
# Fetch single article
curl https://your-worker.workers.dev/articles/getting-started-with-strapi
# Check cache headers
curl -I https://your-worker.workers.dev/articlesThe first request to your Hono application, proxying through to Strapi, processes the response through your configured middleware and business logic.
Subsequent requests leverage caching strategies, such as Hono's middleware patterns or, when deployed to Cloudflare Workers, the Cache API, to serve responses with reduced latency by avoiding repeated calls to your Strapi backend for identical content.
Strapi Open Office Hours
If you have any questions about Strapi or just would like to connect with the Strapi community, you can join us at Strapi's Discord, where the community hosts regular office hours and support discussions.
For more details, visit the Strapi documentation and Hono documentation.
Get Started in Minutes
npx create-strapi-app@latest in your terminal and follow our Quick Start Guide to build your first Strapi project.