Skip to main content

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.

Comments