Přeskočit na hlavní obsah

API Development Guide

This guide explains how to add new API endpoints to the D'n'A Cruises system.

Architecture

The API is built on Cloudflare Workers using a route-based architecture:

  • Location: workers/api/src/
  • Entry Point: workers/api/src/index.ts
  • Routes: workers/api/src/routes/
  • Middleware: workers/api/src/middleware/
  • Utils: workers/api/src/utils/

Adding a New Endpoint

1. Create Route File (if needed)

If creating a new resource, create a new route file:

// workers/api/src/routes/my-resource.ts

/**
* My Resource routes
*
* Routes for managing my resource.
*/

import type { Env } from '../types';
import type { Route } from './index';
import { routes } from './index';
import { requireRole, type AuthContext } from '../middleware/auth';
import { createSuccessResponse, createErrorResponse, parsePaginationParams } from '@dna-cruises/api';

// GET /my-resource
routes.push({
method: 'GET',
path: '/my-resource',
handler: async (request: Request, env: Env) => {
try {
// Your handler logic
return new Response(
JSON.stringify(createSuccessResponse({ data: 'example' })),
{
status: 200,
headers: { 'Content-Type': 'application/json' },
}
);
} catch (error) {
return new Response(
JSON.stringify(createErrorResponse('INTERNAL_ERROR', 'Failed to fetch')),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
}
);
}
},
middleware: [requireRole('Admin')], // Optional: role-based access
});

2. Import Route File

Add the import to workers/api/src/router.ts:

import './routes/my-resource';

3. Route Structure

Each route follows this pattern:

routes.push({
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',
path: '/resource/:id?', // :id is optional
handler: async (request: Request, env: Env) => {
// Handler logic
},
middleware: [/* optional middleware */],
});

Common Patterns

GET with Pagination

routes.push({
method: 'GET',
path: '/items',
handler: async (request: Request, env: Env) => {
try {
const url = new URL(request.url);
const { page, pageSize } = parsePaginationParams(url.searchParams);
const offset = (page - 1) * pageSize;

const items = await env.DB.prepare(
'SELECT * FROM items ORDER BY id DESC LIMIT ? OFFSET ?'
)
.bind(pageSize, offset)
.all();

const totalResult = await env.DB.prepare('SELECT COUNT(*) as total FROM items')
.first<{ total: number }>();
const total = totalResult?.total || 0;

return new Response(
JSON.stringify(
createSuccessResponse({
items: items.results,
pagination: {
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize),
},
})
),
{
status: 200,
headers: { 'Content-Type': 'application/json' },
}
);
} catch (error) {
return new Response(
JSON.stringify(createErrorResponse('INTERNAL_ERROR', 'Failed to fetch items')),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
}
);
}
},
});

POST with Validation

routes.push({
method: 'POST',
path: '/items',
handler: async (request: Request, env: Env) => {
try {
const auth = (request as any).auth as AuthContext;
const body = await request.json() as { name?: string; category_id?: number };
const { name, category_id } = body;

// Validation
if (!name) {
return new Response(
JSON.stringify(createErrorResponse('VALIDATION_ERROR', 'Name is required')),
{
status: 400,
headers: { 'Content-Type': 'application/json' },
}
);
}

const now = Math.floor(Date.now() / 1000);
const result = await env.DB.prepare(
'INSERT INTO items (name, category_id, created_at) VALUES (?, ?, ?) RETURNING *'
)
.bind(name, category_id || null, now)
.first();

return new Response(
JSON.stringify(createSuccessResponse(result)),
{
status: 201,
headers: { 'Content-Type': 'application/json' },
}
);
} catch (error) {
return new Response(
JSON.stringify(createErrorResponse('INTERNAL_ERROR', 'Failed to create item')),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
}
);
}
},
middleware: [requireRole('Admin')],
});

PUT with ID Parameter

routes.push({
method: 'PUT',
path: '/items/:id',
handler: async (request: Request, env: Env) => {
try {
const params = (request as any).params;
const id = parseInt(params.id, 10);
const body = await request.json() as { name?: string };

if (isNaN(id)) {
return new Response(
JSON.stringify(createErrorResponse('INVALID_ID', 'Invalid item ID')),
{
status: 400,
headers: { 'Content-Type': 'application/json' },
}
);
}

// Check if exists
const item = await env.DB.prepare('SELECT * FROM items WHERE id = ?')
.bind(id)
.first();

if (!item) {
return new Response(
JSON.stringify(createErrorResponse('NOT_FOUND', 'Item not found')),
{
status: 404,
headers: { 'Content-Type': 'application/json' },
}
);
}

// Update
const result = await env.DB.prepare(
'UPDATE items SET name = ? WHERE id = ? RETURNING *'
)
.bind(body.name || item.name, id)
.first();

return new Response(
JSON.stringify(createSuccessResponse(result)),
{
status: 200,
headers: { 'Content-Type': 'application/json' },
}
);
} catch (error) {
return new Response(
JSON.stringify(createErrorResponse('INTERNAL_ERROR', 'Failed to update item')),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
}
);
}
},
middleware: [requireRole('Admin')],
});

Authentication & Authorization

Accessing Auth Context

const auth = (request as any).auth as AuthContext;
// auth.userId - User ID
// auth.email - User email
// auth.role - User role (Loader, Producer, Admin)

Role-Based Access

middleware: [requireRole('Admin')], // Only Admin
middleware: [requireRole('Producer', 'Admin')], // Producer or Admin
middleware: [requireRole('Loader', 'Producer', 'Admin')], // All roles

Error Handling

Always use createErrorResponse:

createErrorResponse('ERROR_CODE', 'Human-readable message')

Common error codes:

  • VALIDATION_ERROR - Invalid input
  • NOT_FOUND - Resource not found
  • UNAUTHORIZED - Authentication required
  • FORBIDDEN - Insufficient permissions
  • INTERNAL_ERROR - Server error

Database Queries

Prepared Statements

Always use prepared statements:

const result = await env.DB.prepare('SELECT * FROM items WHERE id = ?')
.bind(id)
.first();

Transactions

For multiple related operations:

// Note: D1 doesn't support explicit transactions
// Use careful ordering and error handling instead

Audit Logging

Log important actions:

const now = Math.floor(Date.now() / 1000);
await env.DB.prepare(
'INSERT INTO audit_log (entity_type, entity_id, action, user_id, timestamp) VALUES (?, ?, ?, ?, ?)'
)
.bind('item', id, 'UPDATE', auth.userId, now)
.run();

Testing

  1. Test locally with wrangler dev
  2. Use curl or Postman for manual testing
  3. Check Cloudflare Dashboard logs for errors

Best Practices

  1. Always validate input - Check required fields and types
  2. Use prepared statements - Prevent SQL injection
  3. Handle errors gracefully - Return proper error responses
  4. Log important actions - Use audit log
  5. Document your endpoints - Add JSDoc comments
  6. Follow existing patterns - Maintain consistency
  7. Test thoroughly - Test all code paths