Business Logic
Organizing shared logic across handlers, listeners, and jobs
All business logic in Arcway lives in your handlers — route handlers, event listeners, and job handlers. Each handler receives a ctx object with all the infrastructure it needs.
Route Handlers
Route handlers receive ctx with req, db, events, cache, queue, files, mail, and log:
// api/users/index.js
export const GET = {
handler: async (ctx) => {
const { db } = ctx;
const users = await db('users').select('id', 'name', 'email');
return { data: users };
},
};
export const POST = {
schema: {
body: {
name: 'string >= 1',
email: 'string.email',
},
},
handler: async (ctx) => {
const { db, events } = ctx;
const [id] = await db('users').insert(ctx.req.body);
await events.emit('users/created', { id, ...ctx.req.body });
return { status: 201, data: { id, ...ctx.req.body } };
},
};Event Listeners
Listeners receive ctx with event, db, and other infrastructure:
// listeners/users/created.js
export default async (ctx) => {
const { event, db, mail } = ctx;
const user = await db('users').where('id', event.payload.id).first();
await mail.send({
to: user.email,
subject: 'Welcome!',
text: `Hi ${user.name}, welcome aboard.`,
});
};Job Handlers
Jobs receive ctx with payload, db, and other infrastructure:
// jobs/generate-report.js
export default {
schedule: '0 9 * * 1', // every Monday at 9am
handler: async (ctx) => {
const { db, mail } = ctx;
const stats = await db('orders')
.where('created_at', '>=', db.raw("date('now', '-7 days')"))
.count('* as total')
.sum('amount as revenue')
.first();
await mail.send({
to: 'team@example.com',
subject: 'Weekly Report',
text: `Orders: ${stats.total}, Revenue: $${stats.revenue}`,
});
},
};Sharing Logic Between Handlers
When multiple handlers need the same logic, extract it into a plain function that receives what it needs as parameters:
// api/users/[id].js
async function findUserOrFail(db, id) {
const user = await db('users').where('id', id).first();
if (!user) return null;
return user;
}
export const GET = {
schema: { query: { id: /^\d+$/ } },
handler: async (ctx) => {
const { db } = ctx;
const user = await findUserOrFail(db, Number(ctx.req.query.id));
if (!user) return { status: 404, error: { code: 'NOT_FOUND', message: 'User not found' } };
return { data: user };
},
};
export const DELETE = {
schema: { query: { id: /^\d+$/ } },
handler: async (ctx) => {
const { db } = ctx;
const user = await findUserOrFail(db, Number(ctx.req.query.id));
if (!user) return { status: 404, error: { code: 'NOT_FOUND', message: 'User not found' } };
await db('users').where('id', ctx.req.query.id).delete();
return { status: 204, data: null };
},
};There's nothing special about where shared functions live — colocate them in the same file, or put them in a separate module and import them. The key is that infrastructure (db, events, etc.) is always passed explicitly as parameters, never imported as globals.