Skip to content

Organizing Backend Code

It’s much easier to move fast as the codebase grows if you’re already using patterns that scale. That’s why we use a layered architecture even if its overkill at the start: Routes → Services → Repositories → Database.

Repositories

Repositories handle database queries. They provide a clean abstraction over a potentially complex schema. For example, fetching messages requires joining across multiple tables (messages, parts, text parts, image parts, tool invocations):

MessageRepository.ts
async function getMessagesByChatIdRaw(chatId: string) {
return db.query.messages.findMany({
where: eq(messages.chatId, chatId),
orderBy: [asc(messages.createdAt)],
with: {
parts: {
orderBy: [asc(messageParts.order)],
with: {
textPart: true,
imagePart: { with: { file: true } },
toolInvocationPart: true,
},
},
},
});
}

The caller doesn’t need to know about the underlying table structure - they just call MessageRepository.getMessagesByChatIdRaw(chatId).

Services

Services handle authorization and transform data for the caller. Here’s the service that uses the repository above:

MessageService.ts
async function getMessagesForChat(chatId: string, userId: string) {
// Verify chat ownership before fetching
const hasAccess = await ChatRepository.verifyChatOwnership(chatId, userId);
if (!hasAccess) {
throw new Error("Unauthorized: Chat not found or access denied");
}
const rawMessages = await MessageRepository.getMessagesByChatIdRaw(chatId);
return transformToNormalizedMessages(rawMessages);
}

The service checks authorization, calls the repository, and transforms the raw database format into something the UI can use.

Server Functions

Server functions are the entry point from the client - they handle auth middleware and call services:

export const getMessages = createServerFn()
.middleware([useSessionTokenClientMiddleware, ensureUserMiddleware])
.inputValidator(getMessagesSchema)
.handler(async ({ data, context }) => {
const messages = await MessageService.getMessagesForChat(
data.chatId,
context.userId,
);
return MessageService.toUIMessages(messages);
});