Maildrone

OpenAPI REST API

External REST API with Hono and automatic OpenAPI documentation

OpenAPI REST API

Maildrone includes a REST API built with Hono and automatic OpenAPI documentation. Perfect for:

  • External integrations (mobile apps, third-party services)
  • Public API for your email marketing platform
  • Webhooks and callbacks
  • Non-TypeScript clients

tRPC vs REST: Use tRPC for your Next.js frontend (type-safe, faster), use REST API for external clients.


API Documentation Options

There are three ways to access API documentation:

1. Swagger UI (/api/v1/doc)

The classic interactive API documentation. Test API calls directly from the browser.

http://localhost:3000/api/v1/doc

📖 Open Swagger UI →

2. OpenAPI JSON (/api/v1/openapi.json)

Raw OpenAPI 3.0 specification. Use this for:

  • Generating SDKs with tools like openapi-generator
  • Importing into Postman, Insomnia, or other API clients
  • CI/CD validation and contract testing
http://localhost:3000/api/v1/openapi.json

📄 View OpenAPI JSON →

3. API Reference (Fumadocs Integration)

Beautiful, searchable API documentation integrated into the docs site using Fumadocs OpenAPI.

Note: The API Reference pages are auto-generated from your OpenAPI schema. Run pnpm openapi:generate to generate them before accessing the links below.

📚 Browse API Reference →


Quick Start

Make Your First Request

# Health check
curl http://localhost:3000/api/v1/health

# Response
{
  "status": "ok",
  "timestamp": "2025-11-17T00:00:00.000Z",
  "version": "1.0.0"
}

Project Structure

src/
└── app/
    └── api/
        └── v1/
            ├── [[...server]]/
            │   ├── route.ts       # Next.js handler (delegates to Hono)
            │   └── app.ts         # Hono app definition
            └── routes/
                ├── index.ts       # Route registration
                ├── health.ts      # Health check endpoint
                └── ...            # Other API endpoints

Creating Endpoints

Basic Endpoint

Create a new route file in src/app/api/v1/routes/:

import { createRoute, z } from '@hono/zod-openapi';
import { app } from '../[[...server]]/app';

// Define OpenAPI schema
const route = createRoute({
  method: 'get',
  path: '/hello',
  summary: 'Say hello',
  tags: ['General'],
  responses: {
    200: {
      description: 'Success',
      content: {
        'application/json': {
          schema: z.object({
            message: z.string(),
            timestamp: z.string(),
          }),
        },
      },
    },
  },
});

// Implement handler
app.openapi(route, (c) => {
  return c.json({
    message: 'Hello from Maildrone!',
    timestamp: new Date().toISOString(),
  });
});

Register in src/app/api/v1/routes/index.ts:

import './hello';  // ← Add this

Now visit: http://localhost:3000/api/v1/hello 🎉


Authentication

API Key Auth

Add authentication middleware:

import { createMiddleware } from 'hono/factory';

const apiKeyAuth = createMiddleware(async (c, next) => {
  const apiKey = c.req.header('X-API-Key');
  
  if (!apiKey) {
    return c.json({ error: 'Missing API key' }, 401);
  }
  
  // Verify API key
  const valid = await verifyApiKey(apiKey);
  if (!valid) {
    return c.json({ error: 'Invalid API key' }, 401);
  }
  
  await next();
});

// Use in routes
app.use('/api/v1/protected/*', apiKeyAuth);

Bearer Token Auth

const bearerAuth = createMiddleware(async (c, next) => {
  const authorization = c.req.header('Authorization');
  
  if (!authorization?.startsWith('Bearer ')) {
    return c.json({ error: 'Missing bearer token' }, 401);
  }
  
  const token = authorization.replace('Bearer ', '');
  
  try {
    const session = await verifyToken(token);
    c.set('session', session);
    await next();
  } catch (err) {
    return c.json({ error: 'Invalid token' }, 401);
  }
});

Validation

Hono uses Zod for validation. All schemas are automatically documented in OpenAPI!

const schema = z.object({
  email: z.string().email('Invalid email format'),
  subject: z.string().min(1, 'Subject is required'),
  content: z.string().min(1, 'Content is required'),
  recipients: z.array(z.string().email()).min(1),
});

app.openapi(route, async (c) => {
  const data = c.req.valid('json');  // ← Already validated!
  
  // data is typed and validated
  console.log(data.email);       // ✅ string
  console.log(data.recipients);  // ✅ string[]
});

Error Handling

Standard Error Responses

// 400 Bad Request
return c.json({ error: 'Invalid input', details: validationErrors }, 400);

// 401 Unauthorized
return c.json({ error: 'Authentication required' }, 401);

// 403 Forbidden
return c.json({ error: 'Insufficient permissions' }, 403);

// 404 Not Found
return c.json({ error: 'Resource not found' }, 404);

// 500 Internal Server Error
return c.json({ error: 'Internal server error' }, 500);

Global Error Handler

// src/app/api/v1/[[...server]]/app.ts
app.onError((err, c) => {
  console.error('API Error:', err);
  
  if (err instanceof ZodError) {
    return c.json({
      error: 'Validation failed',
      details: err.errors,
    }, 400);
  }
  
  return c.json({
    error: 'Internal server error',
    message: process.env.NODE_ENV === 'development' ? err.message : undefined,
  }, 500);
});

CORS

Enable CORS for external clients:

import { cors } from 'hono/cors';

app.use('/api/v1/*', cors({
  origin: ['https://yourdomain.com', 'https://app.yourdomain.com'],
  allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
}));

Development (allow all):

app.use('/api/v1/*', cors({
  origin: '*',  // ⚠️ Only for development!
}));

Best Practices

✅ Do

  • Version your API - Use /api/v1/ prefix
  • Validate all inputs - Use Zod schemas
  • Return consistent errors - Standard error format
  • Document everything - OpenAPI makes it easy
  • Use proper HTTP methods - GET, POST, PUT, DELETE
  • Return proper status codes - 200, 201, 400, 401, 404, 500

❌ Don't

  • Don't skip validation - Always validate inputs
  • Don't expose internal errors - Generic error messages in production
  • Don't return sensitive data - Strip passwords, tokens, etc.
  • Don't ignore security - Use auth, rate limiting, CORS
  • Don't make breaking changes - Version your API instead

Next Steps


Resources

On this page

OpenAPI REST API | Maildrone