Best Boilerplates for Building API Products 2026
·StarterPick Team
apirest-apiopenapihonofastifyboilerplate2026
TL;DR
Building an API product (not just an API) means handling auth, rate limiting, usage billing, versioning, and auto-generated docs — the boring parts take 80% of the time. The best 2026 approach: Hono + OpenAPI spec-first with @hono/zod-openapi, API keys via Unkey, rate limiting via Upstash Ratelimit, and usage billing via Stripe Meters. For GraphQL products: Pothos schema builder + Yoga server. The Hono + Unkey combination has become the default API-as-a-product stack in 2026.
Key Takeaways
- Hono + zod-openapi: Define routes with Zod schemas → auto-generate OpenAPI spec + Swagger UI
- Unkey: Managed API key service (create, revoke, rate limit, usage analytics per key)
- Upstash Ratelimit: Serverless Redis-based rate limiting with sliding window algorithm
- Stripe Meters: Usage-based billing for API calls (per-request or per-token pricing)
- Versioning: Route-based (
/v1/,/v2/) or header-based (API-Version: 2026-01) - Docs generation: OpenAPI → Scalar or Redoc for developer portals
The API Product Stack
Hono (routing)
+ @hono/zod-openapi (schema + OpenAPI)
+ Unkey (API keys + rate limits per key)
+ Upstash Ratelimit (IP-level rate limiting)
+ Stripe Meters (usage billing)
+ Scalar (dev portal / docs UI)
Hono + OpenAPI: Spec-First API
npm create hono@latest my-api
cd my-api
npm install @hono/zod-openapi @hono/swagger-ui zod
npm install @unkey/api @upstash/ratelimit @upstash/redis
// src/routes/packages.ts — spec-first route definition:
import { createRoute, z } from '@hono/zod-openapi';
import { OpenAPIHono } from '@hono/zod-openapi';
const PackageSchema = z.object({
name: z.string().openapi({ example: 'react' }),
version: z.string().openapi({ example: '18.3.0' }),
weeklyDownloads: z.number().openapi({ example: 25000000 }),
description: z.string().nullable(),
}).openapi('Package');
const getPackageRoute = createRoute({
method: 'get',
path: '/packages/{name}',
tags: ['Packages'],
summary: 'Get package details',
request: {
params: z.object({ name: z.string() }),
query: z.object({
period: z.enum(['1w', '1m', '3m', '1y']).optional().default('1m'),
}),
},
responses: {
200: {
content: { 'application/json': { schema: PackageSchema } },
description: 'Package details',
},
404: {
content: {
'application/json': {
schema: z.object({ error: z.string() }),
},
},
description: 'Package not found',
},
429: {
content: {
'application/json': {
schema: z.object({ error: z.string(), retryAfter: z.number() }),
},
},
description: 'Rate limit exceeded',
},
},
});
const router = new OpenAPIHono();
router.openapi(getPackageRoute, async (c) => {
const { name } = c.req.valid('param');
const { period } = c.req.valid('query');
const pkg = await getPackageDetails(name, period);
if (!pkg) return c.json({ error: 'Package not found' }, 404);
return c.json(pkg, 200);
});
API Key Authentication with Unkey
// middleware/apiKey.ts:
import { Unkey } from '@unkey/api';
import { createMiddleware } from 'hono/factory';
const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! });
export const apiKeyAuth = createMiddleware(async (c, next) => {
const apiKey = c.req.header('x-api-key') ??
c.req.header('Authorization')?.replace('Bearer ', '');
if (!apiKey) {
return c.json({ error: 'API key required. Get one at dashboard.yourdomain.com' }, 401);
}
const { result, error } = await unkey.keys.verify({ key: apiKey });
if (error || !result.valid) {
return c.json({
error: result?.code === 'RATE_LIMITED'
? 'Rate limit exceeded'
: 'Invalid API key'
}, result?.code === 'RATE_LIMITED' ? 429 : 401);
}
// Attach key metadata to context:
c.set('apiKeyId', result.keyId);
c.set('userId', result.ownerId);
c.set('plan', result.meta?.plan ?? 'free');
c.set('remaining', result.remaining);
await next();
});
// Creating API keys for users (in your dashboard):
import { Unkey } from '@unkey/api';
const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! });
async function createApiKey(userId: string, plan: 'free' | 'pro' | 'enterprise') {
const limits = {
free: { ratelimit: { type: 'fast', limit: 100, refillRate: 100, refillInterval: 60000 } },
pro: { ratelimit: { type: 'fast', limit: 1000, refillRate: 1000, refillInterval: 60000 } },
enterprise: null, // No limit
};
const { result } = await unkey.keys.create({
apiId: process.env.UNKEY_API_ID!,
ownerId: userId,
meta: { plan },
prefix: 'pk',
...limits[plan] && { ratelimit: limits[plan].ratelimit },
});
return result!.key; // Return the key to show to user ONCE
}
Usage-Based Billing with Stripe Meters
// lib/billing.ts:
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
// Record API usage for billing:
export async function recordApiUsage(
stripeCustomerId: string,
endpoint: string,
quantity: number = 1
) {
await stripe.billing.meterEvents.create({
event_name: 'api_requests',
payload: {
stripe_customer_id: stripeCustomerId,
value: String(quantity),
},
identifier: `${stripeCustomerId}-${Date.now()}`, // Idempotency
});
}
// middleware/usageTracking.ts:
export const trackUsage = createMiddleware(async (c, next) => {
await next();
// Only track successful responses:
if (c.res.status >= 200 && c.res.status < 300) {
const userId = c.get('userId');
const endpoint = c.req.routePath;
// Fire and forget — don't block response:
if (userId) {
const stripeId = await getUserStripeId(userId);
recordApiUsage(stripeId, endpoint).catch(console.error);
}
}
});
Rate Limiting with Upstash
// middleware/rateLimit.ts (IP-level, before API key check):
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '10 s'), // 10 req per 10 seconds globally
analytics: true,
prefix: '@upstash/ratelimit',
});
export const ipRateLimit = createMiddleware(async (c, next) => {
const ip = c.req.header('x-forwarded-for')?.split(',')[0] ?? 'anonymous';
const { success, limit, remaining, reset } = await ratelimit.limit(ip);
c.header('X-RateLimit-Limit', String(limit));
c.header('X-RateLimit-Remaining', String(remaining));
c.header('X-RateLimit-Reset', String(reset));
if (!success) {
return c.json({
error: 'Too many requests',
retryAfter: Math.ceil((reset - Date.now()) / 1000),
}, 429);
}
await next();
});
OpenAPI Docs with Scalar
// src/app.ts — full app setup:
import { OpenAPIHono } from '@hono/zod-openapi';
import { apiReference } from '@scalar/hono-api-reference';
import { packagesRouter } from './routes/packages';
import { apiKeyAuth } from './middleware/apiKey';
import { ipRateLimit } from './middleware/rateLimit';
import { trackUsage } from './middleware/usageTracking';
const app = new OpenAPIHono();
// Global middleware:
app.use('*', ipRateLimit);
app.use('/v1/*', apiKeyAuth);
app.use('/v1/*', trackUsage);
// Routes:
app.route('/v1', packagesRouter);
// OpenAPI spec endpoint:
app.doc('/openapi.json', {
openapi: '3.1.0',
info: {
version: '1.0.0',
title: 'My API',
description: 'API for developers',
},
servers: [
{ url: 'https://api.yourdomain.com', description: 'Production' },
{ url: 'http://localhost:3000', description: 'Local' },
],
});
// Scalar developer docs:
app.get('/docs', apiReference({ spec: { url: '/openapi.json' } }));
export default app;
Versioning Strategy
// Route-based versioning (recommended):
const v1Router = new OpenAPIHono();
const v2Router = new OpenAPIHono();
app.route('/v1', v1Router);
app.route('/v2', v2Router);
// Deprecation headers:
v1Router.use('*', async (c, next) => {
await next();
c.header('Deprecation', 'true');
c.header('Sunset', 'Sat, 01 Jan 2027 00:00:00 GMT');
c.header('Link', '</v2/packages>; rel="successor-version"');
});
API Product Feature Checklist
Core (must have):
✅ API key authentication (Unkey)
✅ Rate limiting per key + IP (Upstash)
✅ OpenAPI spec + Swagger/Scalar docs
✅ Versioned routes (/v1/, /v2/)
✅ Consistent error format { error, code, details }
Growth (recommended):
✅ Usage billing (Stripe Meters)
✅ Usage dashboard for API key holders
✅ SDK generation from OpenAPI spec (openapi-ts)
✅ Webhook events for async operations
✅ Status page (Statuspage.io or BetterStack)
Enterprise (later):
⬜ IP allowlisting per key
⬜ Audit logs
⬜ SSO / SAML authentication
⬜ Private API deployment
⬜ SLA tiers with dedicated rate limits
Find API product boilerplates at StarterPick.