YeboLink Controllers Deep Dive
Controllers in YeboLink handle HTTP request/response logic and delegate business logic to services. They are located in src/controllers/.
Controller Architecture
YeboLink uses a hybrid approach:
- Most routes use inline async handlers in route files
- Dashboard & Blog use dedicated controller classes
This pattern keeps simple CRUD operations lightweight while extracting complex logic.
DashboardController (dashboard.controller.ts)
Handles platform-wide metrics for the CEO dashboard.
Methods
getMetrics(req, res)
Returns real-time platform statistics.
typescript
import { Request, Response } from 'express';
import dashboardService from '../services/dashboard.service';
class DashboardController {
async getMetrics(req: Request, res: Response) {
try {
const metrics = await dashboardService.getMetrics();
return res.json({
success: true,
data: metrics
});
} catch (error: any) {
console.error('[Dashboard Controller] Error fetching metrics:', error);
return res.status(500).json({
success: false,
error: 'Failed to fetch dashboard metrics'
});
}
}
}
export default new DashboardController();Response Structure:
json
{
"success": true,
"data": {
"messagesSent": 150,
"messagesTrend": "up",
"messagesChange": 25.5,
"newContacts": 42,
"contactsTrend": "up",
"contactsChange": 10.2,
"deliveryRate": 98.5,
"deliveryTrend": "neutral",
"deliveryChange": 0.5,
"activeWorkspaces": 12,
"workspacesTrend": "up",
"workspacesChange": 8.3
}
}BlogController (blog.controller.ts)
Handles blog post CRUD for the autoblogger integration.
Methods
createBlogPost(req, res)
Creates a new blog post (requires API key auth).
typescript
async createBlogPost(req: Request, res: Response) {
try {
// Validate API key
const apiKey = req.headers['x-api-key'] as string;
const expectedKey = process.env.BLOG_API_KEY;
if (!apiKey || apiKey !== expectedKey) {
return res.status(401).json({
success: false,
error: 'Invalid or missing API key'
});
}
const { title, slug, content, excerpt, category, tags, status, author, featured_image, published_at } = req.body;
// Validate required fields
if (!title || !slug || !content) {
return res.status(400).json({
success: false,
error: 'Missing required fields: title, slug, and content are required'
});
}
const blogPost = await blogService.createBlogPost({
title,
slug,
content,
excerpt,
category,
tags,
status,
author,
featured_image,
published_at
});
return res.status(201).json({
success: true,
data: blogPost
});
} catch (error: any) {
if (error.message === 'Blog post with this slug already exists') {
return res.status(409).json({
success: false,
error: error.message
});
}
console.error('[Blog Controller] Error creating blog post:', error);
return res.status(500).json({
success: false,
error: 'Failed to create blog post'
});
}
}getBlogPosts(req, res)
Returns paginated list of published posts.
typescript
async getBlogPosts(req: Request, res: Response) {
try {
const page = parseInt(req.query.page as string) || 1;
const limit = Math.min(parseInt(req.query.limit as string) || 10, 50);
const category = req.query.category as string | undefined;
const result = await blogService.getPublishedBlogPosts(page, limit, category);
return res.json({
success: true,
data: result.posts,
pagination: {
page,
limit,
total: result.total,
totalPages: result.totalPages,
hasNext: result.hasNext,
hasPrev: result.hasPrev
}
});
} catch (error: any) {
console.error('[Blog Controller] Error fetching blog posts:', error);
return res.status(500).json({
success: false,
error: 'Failed to fetch blog posts'
});
}
}getBlogPostBySlug(req, res)
Returns a single post by slug.
typescript
async getBlogPostBySlug(req: Request, res: Response) {
try {
const { slug } = req.params;
const blogPost = await blogService.getBlogPostBySlug(slug);
if (!blogPost) {
return res.status(404).json({
success: false,
error: 'Blog post not found'
});
}
return res.json({
success: true,
data: blogPost
});
} catch (error: any) {
console.error('[Blog Controller] Error fetching blog post:', error);
return res.status(500).json({
success: false,
error: 'Failed to fetch blog post'
});
}
}Inline Route Handlers
Most YeboLink endpoints use inline async handlers. Here are key patterns:
Pattern: Service Delegation
typescript
// routes/messages.routes.ts
router.post(
'/send',
authenticateAny,
apiKeyRateLimiter,
sendMessageValidation,
validate,
asyncHandler(async (req: Request, res: Response) => {
const message = await MessageService.sendMessage(
req.workspaceId!,
req.apiKeyId,
req.body
);
res.status(201).json({
success: true,
data: {
message_id: message.id,
status: message.status,
credits_used: message.credits_used,
created_at: message.created_at,
},
});
})
);Pattern: Direct Database Query
typescript
// routes/dashboard.routes.ts
router.get(
'/workspaces',
asyncHandler(async (req: Request, res: Response) => {
const result = await db.query(`
SELECT id, name, email, phone, country, credits_balance,
sms_sender_name, email_verified, is_active,
onboarding_completed, company_name, created_at, updated_at
FROM workspaces
ORDER BY created_at DESC
`);
// Get message counts per workspace
const msgCounts = await db.query(`
SELECT workspace_id, COUNT(*) as total_messages,
SUM(CASE WHEN status = 'delivered' THEN 1 ELSE 0 END) as delivered,
SUM(credits_used) as total_credits_used
FROM messages
GROUP BY workspace_id
`);
const countMap = new Map(msgCounts.rows.map((r: any) => [r.workspace_id, r]));
const workspaces = result.rows.map((w: any) => ({
...w,
stats: countMap.get(w.id) || { total_messages: 0, delivered: 0 },
}));
res.json({ success: true, data: { workspaces, total: workspaces.length } });
})
);Pattern: Webhook Handler
typescript
// routes/webhooks.routes.ts
router.post(
'/twilio/status',
asyncHandler(async (req: Request, res: Response) => {
// Validate Twilio signature
const signature = req.headers['x-twilio-signature'] as string;
const url = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
const isValid = TwilioService.validateWebhookSignature(
signature, url, req.body
);
if (!isValid) {
res.status(403).send('Invalid signature');
return;
}
// Parse and process
const statusData = TwilioService.parseStatusCallback(req.body);
const message = await MessageModel.findByProviderMessageId(statusData.messageSid);
if (message) {
await MessageModel.updateStatus(message.id, statusData.status, {
error_message: statusData.errorMessage,
});
await WebhookService.trigger(message.workspace_id, `message.${statusData.status}`, {
message_id: message.id,
channel: message.channel,
recipient: message.recipient,
status: statusData.status,
});
}
res.status(200).send('OK');
})
);Pattern: File/Form Handling
typescript
// routes/billing.routes.ts
router.post(
'/webhook',
asyncHandler(async (req: Request, res: Response) => {
const signature = req.headers['stripe-signature'] as string;
if (!signature) {
res.status(400).json({
success: false,
error: 'Missing stripe-signature header',
});
return;
}
// Raw body preserved by express.json() verify callback
const payload = (req as any).rawBody || Buffer.from(JSON.stringify(req.body));
await BillingService.handleWebhook(signature, payload);
res.json({ received: true });
})
);Error Handling in Controllers
All controllers use the asyncHandler wrapper for consistent error handling:
typescript
// middleware/errorHandler.ts
export const asyncHandler = (
fn: (req: Request, res: Response, next: NextFunction) => Promise<any>
) => {
return (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};Errors bubble to the global error handler:
typescript
export const errorHandler = (
err: Error | AppError,
req: Request,
res: Response,
next: NextFunction
): void => {
let statusCode = 500;
let message = 'Internal server error';
if (err instanceof AppError) {
statusCode = err.statusCode;
message = err.message;
}
logger.error('Error occurred', {
message: err.message,
stack: err.stack,
path: req.path,
workspaceId: req.workspaceId,
});
res.status(statusCode).json({
success: false,
error: message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
});
};Controller Best Practices
- Validate inputs using express-validator middleware before handler
- Delegate business logic to services, never put it in controllers
- Use consistent response format:
{ success: boolean, data?: T, error?: string } - Log at service level, not controller level
- Handle edge cases (not found, already exists) with appropriate status codes
- Sanitize outputs — never return
password_hashor sensitive fields