Skip to content

API Conventions ​

All system / business endpoints share one enforced convention; deviations require discussion.

Full API reference: Apidog. This page covers conventions and a key endpoint cheat sheet.

Route prefixes ​

PrefixPurpose
/api/v1/authAuthentication (public)
/api/v1/routeRoute delivery (constant routes / user routes)
/api/v1/system-manage/*System modules (users / roles / menus / APIs / dictionary)
/api/v1/business/<module>/*Business modules (autodiscovered)

Response format ​

All success responses are:

json
{ "code": "0000", "msg": "OK", "data": { ... } }

Field naming is uniformly camelCase — SchemaBase's alias_generator=to_camel_case handles it; Model.to_dict() outputs camelCase too. Don't return raw dicts; don't hand-roll snake_case.

For codes / frontend mapping see Response codes.

Path & method ​

OperationMethod + pathBody / Params
List / searchPOST /resources/searchBody extends PageQueryBase
SingleGET /resources/{id}—
CreatePOST /resourcesBody: XxxCreate
UpdatePATCH /resources/{id}Body: XxxUpdate
Delete oneDELETE /resources/{id}—
Batch deleteDELETE /resourcesBody: {ids: [...]} (CommonIds)
Sub-resourceGET /resources/{id}/sub—
Derived queryGET /resources/tree, GET /resources/options—
Instance actionPOST /resources/{id}/action-nameoptional body
Collection actionPOST /resources/batch-offline, POST /resources/refreshbody

Constraints:

  • No trailing slashes (/users ✅, /users/ ❌)
  • Multi-word paths use kebab-case (/batch-offline, /constant-routes, /user-routes)
  • Resource names are plural (/users, /roles, /warehouses)
  • "Search" uniformly uses POST /resources/search instead of GET ?...=... — supports complex bodies (arrays, nesting)

Field naming ​

  • Request body / query: camelCase (Pydantic validate_by_name=True accepts snake_case too, but the frontend always sends camelCase)
  • Response data: camelCase — use schema.model_dump(by_alias=True) or model.to_dict()

Pagination ​

Body extends PageQueryBase:

python
from app.utils import PageQueryBase

class WarehouseSearch(PageQueryBase):
    name: str | None = None

Fixed fields:

FieldDefaultPurpose
current1page (≥ 1)
size10page size (1–1000)
orderBynullsort fields, - prefix = desc, e.g. ["-createdAt", "id"]

Response:

json
{
  "code": "0000", "msg": "OK",
  "data": {
    "records": [...],
    "total": 42,
    "current": 1,
    "size": 10
  }
}

IDs are sqids ​

All public-facing resource IDs are sqid strings (e.g. Yc7vN3kE) — never raw auto-increment ints. Use SqidId in Pydantic schemas and SqidPath for path params:

python
from app.utils import SqidId, SqidPath, SchemaBase

class WarehouseUpdate(SchemaBase):
    parent_id: SqidId | None = None        # body field

@router.get("/warehouses/{item_id}")
async def get_dept(item_id: SqidPath):     # path param
    ...

SqidId decodes the sqid to int on input and re-encodes to sqid on output. SqidPath decodes only. See Sqids.

CRUDRouter — don't hand-write ​

The default way to add a resource's routes is CRUDRouter — 6 standard REST routes in a single declaration:

python
from app.utils import CRUDRouter, SearchFieldConfig, require_buttons

dept_crud = CRUDRouter(
    prefix="/warehouses",
    controller=warehouse_controller,
    create_schema=WarehouseCreate,
    update_schema=WarehouseUpdate,
    list_schema=WarehouseSearch,
    search_fields=SearchFieldConfig(
        contains_fields=["name", "code"],
        exact_fields=["status"],
    ),
    summary_prefix="Warehouse",
    soft_delete=True,
    tree_endpoint=True,
    action_dependencies={
        "create": [require_buttons("B_INVENTORY_WAREHOUSE_CREATE")],
        "update": [require_buttons("B_INVENTORY_WAREHOUSE_EDIT")],
        "delete": [require_buttons("B_INVENTORY_WAREHOUSE_DELETE")],
        "batch_delete": [require_buttons("B_INVENTORY_WAREHOUSE_DELETE")],
    },
)
router = dept_crud.router

To customize a route, use @crud.override("list") — don't redeclare the same path on the router; it'll be shadowed by _OrderedRouter reordering. Full API in CRUDRouter.

Auth dependencies ​

DependencyPurpose
DependAuthJWT validation only, no permission check (captcha, impersonate, user info)
DependPermissionOn top of DependAuth, matches (method, path) against role.apis (default for business endpoints)
require_buttons("B_X", ...)Any one passes; otherwise 2203
require_buttons(..., require_all=True)All required; otherwise 2202
require_roles("R_X", ...)Same as buttons but for roles; codes 2204 / 2205

R_SUPER bypasses everything. See Auth.

Endpoint cheat sheet ​

Auth (/api/v1/auth, public) ​

MethodPathPurpose
POST/loginUsername + password
POST/captchaSend phone captcha
POST/code-loginCaptcha login
POST/registerRegister (default role R_USER)
POST/refresh-tokenRefresh access token
GET/user-infoCurrent user info + roles + buttons (DependAuth)
PATCH/passwordChange password (DependAuth, increments token version)
POST/impersonate/{user_id}Super-admin impersonate (DependPermission + super check)

Route (/api/v1/route) ​

MethodPathPurpose
GET/constant-routesPublic routes (login / error pages) from Redis
GET/user-routesCurrent user's menu tree (DependAuth)
GET/exists?name=xxxCheck if a route name exists (DependAuth)

System manage (/api/v1/system-manage, all behind DependPermission) ​

Each follows the standard 6-route convention:

ResourcePrefixNotes
Users/userscreate / update are @overrided for password hashing + role association
Roles/rolesIncludes GET /roles/{id}/menus, PATCH /roles/{id}/menus, etc.
Menus/menusIncludes GET /menus/tree, GET /menus/pages
APIs/apisRead-only (list / get / tree / tags only); records are reconciled by refresh_api_list() at startup
Dictionary/dictionariesIncludes GET /dictionaries/{type}/options (5-min Redis cache)

Business modules (/api/v1/business/<name>) ​

Per-module. Business module endpoints are maintained by each module api/ package and init data.

Response wrappers ​

ClassPurposeTypical scenario
Success(data=...)Single / non-paginatedget / create / update
SuccessExtra(data={"records": [...]}, total, current, size)Paginatedlist / search
Fail(code=Code.X, msg="...")Business failurerule violation
Custom(code, status_code, msg, data, **kwargs)Anythingrare custom status_code

OpenAPI response model

Success etc. are JSONResponse subclasses; FastAPI cannot infer the response shape. To document accurately, add response_model=ResponseModel[UserOut] or PageResponseModel[UserOut]. See Schema base.

Static-path priority inside the router ​

CRUDRouter uses _OrderedRouter: every add_api_route triggers a re-sort that moves paths without {...} to the front. This prevents GET /resources/{id} from shadowing later-added GET /resources/tree. Don't bypass this (e.g. by mutating router.routes.append(...)).

基于 MIT 协议发布