API service modules
Domain services live under apps/web/src/lib/services/. Each module wraps a slice of the Workers API using request(). They are the only place pages and query hooks should go for HTTP.
Related: API core · Query · Permissions.
Responsibilities
A service module does:
- Map a method name → HTTP method + path
- Map params / body to Axios config
- Return a stable frontend type (often unwrapped nested fields, e.g.
res.user) - Stay free of React, routing, and permission UI decisions
A service module does not:
- Own loading / error UI state (that is Query + page)
- Decide whether a button is visible (that is permissions)
- Cache server data (that is TanStack Query)
Module map
Exported from apps/web/src/lib/services/index.ts:
| Service | Typical routes | Notes |
|---|---|---|
authService | /auth/* | Login, register, logout, verify |
meService | /me/* | Profile, settings, security surfaces |
sessionService | /me/session* | Session list / revoke |
avatarService | avatar upload / get | Multipart where needed |
usersService | /users* | List, create, detail, permissions, activities |
departmentsService | /departments* | Org departments |
maintenanceService | /maintenance* | Lifecycle CRUD + workflow actions |
systemService | /system* | Permissions catalog, monitor, logs, audit, settings |
notificationsService | notifications | Inbox / unread |
dashboardService | dashboard analytics | Role dashboards (when used) |
Add a new folder under lib/services/<domain>/ when a new API domain appears — do not dump unrelated methods into users or system.
Conventions
typescript
export const usersService = {
async getUsers(params?: UsersListParams, options?: { handleErrors?: boolean }) {
return request<UsersListResult>({
method: 'GET',
url: '/users',
params: /* stringified query */,
...options,
});
},
async createUser(payload: CreateUserPayload, options?: { handleErrors?: boolean }) {
const res = await request<{ user: User }>({
method: 'POST',
url: '/users/new',
data: payload,
...options,
});
return res.user; // unwrap nested envelope fields for callers
},
};- Prefer named options
{ handleErrors?, context? }on every public method. - Keep endpoint-specific types next to the service (or in
apps/web/src/types/). - Document permission requirements in JSDoc when the Worker enforces them (
user.create, etc.).
How this connects to Query
- Service = transport
src/query/keys.ts+ domain hooks = cache keys + invalidation- Page = UI wiring
Example: useUsersListQuery calls usersService.getUsers; mutations invalidate queryKeys.users.lists() / detail(id). See Query.
Checklist for a new endpoint
- Confirm Worker route + Zod schema exist
- Add / extend the domain service method
- Add query key + hook if the page needs cached server state
- Gate UI with permissions; never rely on hiding the call alone