Skip to main content
Advertisement

13.6 Pro Tips — HTTP Error Types, OpenAPI Auto-generation, Hono

Common HTTP Error Type Design

// types/errors.ts

// HTTP status code types
type HttpStatusCode =
| 200 | 201 | 204
| 400 | 401 | 403 | 404 | 409 | 422 | 429
| 500 | 502 | 503

// Error code enum
type ErrorCode =
| 'VALIDATION_ERROR'
| 'UNAUTHORIZED'
| 'FORBIDDEN'
| 'NOT_FOUND'
| 'CONFLICT'
| 'RATE_LIMITED'
| 'INTERNAL_ERROR'

// Base error response format
interface ApiErrorResponse {
error: {
code: ErrorCode
message: string
details?: Record<string, string[]>
requestId?: string
timestamp: string
}
}

// Error class hierarchy
class HttpError extends Error {
constructor(
public readonly statusCode: HttpStatusCode,
public readonly code: ErrorCode,
message: string,
public readonly details?: Record<string, string[]>
) {
super(message)
this.name = 'HttpError'
}

toResponse(): ApiErrorResponse {
return {
error: {
code: this.code,
message: this.message,
details: this.details,
timestamp: new Date().toISOString(),
},
}
}
}

class ValidationError extends HttpError {
constructor(details: Record<string, string[]>) {
super(422, 'VALIDATION_ERROR', 'Invalid input values.', details)
}
}

class UnauthorizedError extends HttpError {
constructor(message = 'Authentication required.') {
super(401, 'UNAUTHORIZED', message)
}
}

class ForbiddenError extends HttpError {
constructor(message = 'Access denied.') {
super(403, 'FORBIDDEN', message)
}
}

class NotFoundError extends HttpError {
constructor(resource: string) {
super(404, 'NOT_FOUND', `${resource} not found.`)
}
}

class ConflictError extends HttpError {
constructor(message: string) {
super(409, 'CONFLICT', message)
}
}

OpenAPI Type Auto-generation (openapi-typescript)

Automatically generate TypeScript types from REST API specs.

npm install --save-dev openapi-typescript
# Generate types from OpenAPI spec
npx openapi-typescript ./openapi.yaml --output src/generated/api.ts

# From remote URL
npx openapi-typescript https://api.example.com/openapi.json --output src/generated/api.ts
// Using generated types (with openapi-fetch)
import createClient from 'openapi-fetch'
import type { paths } from './generated/api'

const client = createClient<paths>({ baseUrl: 'https://api.example.com' })

// Type-safe API calls
const { data, error } = await client.GET('/users/{id}', {
params: {
path: { id: '123' }, // Path parameter
query: { include: 'posts' }, // Query parameter
},
})

// data: paths['/users/{id}']['get']['responses']['200']['content']['application/json']
// error: error response type

Hono — Ultra-lightweight Framework for Edge/Fullstack

Hono is a TypeScript-first framework that is much lighter and faster than Express.

npm install hono
// src/app.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { jwt } from 'hono/jwt'
import { z } from 'zod'

const app = new Hono()

// Type-safe environment variables
type Env = {
Bindings: {
DATABASE_URL: string
JWT_SECRET: string
}
Variables: {
userId: string
userRole: 'admin' | 'user'
}
}

const api = new Hono<Env>()

// Middleware
api.use('/protected/*', jwt({ secret: 'secret' }))

// Zod validation
const CreateUserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
})

api.post(
'/users',
zValidator('json', CreateUserSchema), // Auto-validate + type inference
async (c) => {
const data = c.req.valid('json') // CreateUserSchema type-safe
const user = await createUser(data)
return c.json(user, 201)
}
)

api.get('/users/:id', async (c) => {
const id = c.req.param('id') // string
const user = await getUser(id)

if (!user) {
return c.json({ error: 'User not found' }, 404)
}

return c.json(user)
})

// RPC style (generate client types)
const routes = app
.get('/health', (c) => c.json({ status: 'ok' }))
.get('/users', async (c) => c.json(await getUsers()))
.post('/users', zValidator('json', CreateUserSchema), async (c) => {
const data = c.req.valid('json')
return c.json(await createUser(data), 201)
})

// Extract client type
export type AppType = typeof routes

Hono Client (Type Sharing)

// client/api.ts
import { hc } from 'hono/client'
import type { AppType } from '../server/app'

const client = hc<AppType>('http://localhost:3000')

// Fully type-safe API calls
const res = await client.users.$get()
const users = await res.json() // Types automatically inferred

const created = await client.users.$post({
json: { name: 'Alice', email: 'alice@example.com' },
})

Common API Client Pattern

// lib/api-client.ts
class ApiClient {
constructor(
private baseUrl: string,
private getToken: () => string | null
) {}

private async request<T>(
method: string,
path: string,
options?: {
body?: unknown
params?: Record<string, string>
}
): Promise<T> {
const url = new URL(path, this.baseUrl)

if (options?.params) {
Object.entries(options.params).forEach(([key, value]) => {
url.searchParams.set(key, value)
})
}

const token = this.getToken()
const headers: HeadersInit = {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
}

const response = await fetch(url, {
method,
headers,
body: options?.body ? JSON.stringify(options.body) : undefined,
})

if (!response.ok) {
const error: ApiErrorResponse = await response.json()
throw new Error(error.error.message)
}

return response.json() as T
}

get<T>(path: string, params?: Record<string, string>) {
return this.request<T>('GET', path, { params })
}

post<T>(path: string, body: unknown) {
return this.request<T>('POST', path, { body })
}

put<T>(path: string, body: unknown) {
return this.request<T>('PUT', path, { body })
}

delete<T>(path: string) {
return this.request<T>('DELETE', path)
}
}

// Usage
const api = new ApiClient('https://api.example.com', () => localStorage.getItem('token'))

const users = await api.get<User[]>('/users')
const user = await api.post<User>('/users', { name: 'Alice', email: 'alice@example.com' })

Pro Tips

Framework Selection Guide

Express:     Legacy projects, vast ecosystem, slow startup
Fastify: Performance-critical, schema-based validation
NestJS: Large teams, DI needed, enterprise
Hono: Edge deployment, small fast APIs, type safety
Advertisement