Backend Guide
A practical guide to src/backend responsibilities, api-client usage in client/server contexts, and the full request chain.
This guide focuses on MuseMVP customization. It shows where to change code and how to connect the full backend flow in this template.
src/backend quick map
| Directory | Responsibility | Typical files |
|---|---|---|
src/backend/api | Hono API entry, routers, middleware, OpenAPI | api/app.ts, api/routes/*, api/middleware/* |
src/backend/api-client | Reusable API calling layer (hooks + server helpers) | api-client.ts, admin/use-admin.ts, auth/use-auth-server.ts |
src/backend/auth | Better-Auth server/client setup | auth/auth.ts, auth/client.ts |
src/backend/database | Drizzle client, schema, queries | database/client.ts, database/schema.ts, database/queries/* |
Important Boundary
src/backend provides shared backend capabilities. Business orchestration should mainly live in src/modules/<feature>/lib/*, then be called from API routes.
How a request flows end to end
A client component sends a request (usually via hooks in src/backend/api-client/*).
The request enters /api/* and is forwarded by src/app/api/[[...rest]]/route.ts to Hono.
src/backend/api/app.ts runs global middleware (logger, CORS) and dispatches to the matched router.
Route middleware (such as authMiddleware, adminMiddleware) runs; handler performs validation and shapes responses.
Handler calls service (src/modules/*) or query (src/backend/database/queries/*).
Database result returns to frontend; for mutations, TanStack Query cache invalidation keeps UI fresh.
How to use api-client (client vs server)
Client side (Client Components): prefer hooks
You already have reusable hooks (for example useAdminUsersQuery, useAvatarUploadMutation). This is the default path:
"use client";
import { useAdminUsersQuery } from "@/backend/api-client/admin/use-admin";
export function AdminUsersPanel() {
const { data, isLoading, error } = useAdminUsersQuery({
itemsPerPage: 10,
currentPage: 1,
searchTerm: "",
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Load failed</div>;
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}If you create a new hook, combine apiClient with InferRequestType to avoid manual request types:
import { useMutation } from "@tanstack/react-query";
import type { InferRequestType } from "hono";
import { apiClient } from "@/backend/api-client/api-client";
type ContactForm = InferRequestType<typeof apiClient.contact.$post>["form"];
export function useContactSubmit() {
return useMutation({
mutationFn: async (form: ContactForm) => {
const res = await apiClient.contact.$post({ form });
if (!res.ok) throw new Error("Submit failed");
},
});
}Server side (Server Components / Route Handlers): two strategies
- In monolith scenarios, prefer direct service/query calls (no extra HTTP hop):
import { getSession } from "@/backend/api-client/auth/use-auth-server";
import { getActiveMuseBillingContractsByUserId } from "@/backend/database";
export async function getCurrentUserProState() {
const session = await getSession();
if (!session?.user?.id) return { hasProAccess: false };
const contracts = await getActiveMuseBillingContractsByUserId(session.user.id);
return { hasProAccess: contracts.length > 0 };
}- If server code must call
/api/*, forward cookies explicitly (common inNextRequestcontexts):
import type { NextRequest } from "next/server";
import { getBaseUrl } from "@/lib/get-base-url";
export async function getMuseAccessStatusForSession(req: NextRequest) {
const response = await fetch(`${getBaseUrl()}/api/muse-billing/access`, {
headers: {
cookie: req.headers.get("cookie") || "",
},
});
if (!response.ok) return null;
return response.json();
}Recommended flow when adding a new API feature
Add DB read/write in src/backend/database/queries/* (DB-only).
Add business orchestration in src/modules/<feature>/lib/* (rules, transitions, errors).
Add Hono route in src/backend/api/routes/<feature>/* with validator + middleware + handler.
Register router in src/backend/api/app.ts.
Expose frontend hooks in src/backend/api-client/<feature>/*, and run invalidateQueries after mutations.
Verify contract output in /api/openapi or /api/docs.