with one click
api-guide
// Guide for implementing REST and GraphQL APIs (create, get, search, update, delete, purge, scope prefix patterns, admin_ prefix, SearchScope, BaseFilterAdapter, @api_function, Click CLI)
// Guide for implementing REST and GraphQL APIs (create, get, search, update, delete, purge, scope prefix patterns, admin_ prefix, SearchScope, BaseFilterAdapter, @api_function, Click CLI)
Guide for implementing Backend.AI repository patterns (create, get, search, update, delete, purge, batch operations, Querier, BatchQuerier, Creator, Updater, Purger, SearchScope, with_tables)
Diagnose and fix Docker Compose halfstack issues — config mapping, service health, DB/Redis/etcd inspection, supergraph regeneration
Local development tools — service management (./dev) and v2 CLI testing (./bai)
Guide for implementing Backend.AI client SDK and CLI (Session, BaseFunction, @api_function, Click commands, Pydantic models, FieldSpec, output handlers, APIConfig, testing)
Complete submission workflow - quality checks, commit, PR creation, changelog generation, and final push. Use after finishing implementation work.
Guide the Backend.AI release process - run release.sh, generate changelog via towncrier, consolidate RC entries for final releases with subsection grouping.
| name | api-guide |
| description | Guide for implementing REST and GraphQL APIs (create, get, search, update, delete, purge, scope prefix patterns, admin_ prefix, SearchScope, BaseFilterAdapter, @api_function, Click CLI) |
| version | 1.0.0 |
| dependencies | ["service-guide","repository-guide"] |
| tags | ["rest-api","graphql","client-sdk","cli","api-patterns"] |
Guide for implementing REST and GraphQL APIs with standard operations and scope patterns.
APIs implement 6 standard operations:
Batch operations: batch_update, batch_delete, batch_purge
API layer only (Service/Repository layers don't use prefix)
Operations within a scope use {scope}_ prefix:
REST:
POST /domains/{domain}/users → domain_create_user
GET /domains/{domain}/users/{id} → domain_user
GET /domains/{domain}/users → domain_search_users
PATCH /domains/{domain}/users/{id} → domain_update_user
DELETE /domains/{domain}/users/{id} → domain_delete_user
GraphQL:
mutation domainCreateUser(scope: DomainScope, input: ...)
query domainUser(scope: DomainScope, id: ID)
query domainSearchUsers(scope: DomainScope, filter: ...)
Operations without scope use admin_ prefix (superadmin required):
REST:
POST /admin/domains → admin_create_domain
GET /admin/domains/{id} → admin_domain
GET /admin/domains → admin_search_domains
GraphQL:
mutation adminCreateDomain(input: ...)
query adminDomain(id: ID)
query adminSearchDomains(filter: ...)
search — always two variants:
admin_search_*: superadmin, no scope — queries entire system.{scope}_search_*: non-admin, scope required — queries within the given scope only.create / update / get / delete / purge — depends on the entity:
admin_ endpoint.admin_ and non-admin endpoints with different DTOs.admin_ variant.New endpoints MUST use REST v2 patterns below. REST v1 is for existing legacy handlers only — do not add new handlers here.
REST v1 Handler → Processor → Service → Repository
Key Files:
src/ai/backend/manager/api/rest/{domain}/handler.py - Handlerssrc/ai/backend/manager/api/rest/{domain}/adapter.py - Filters (per-domain)src/ai/backend/manager/api/rest/adapter.py - Base adapterSee complete examples:
api/rest/fair_share/handler.py - Handler calling processorservices/processors.py - ActionProcessor base classREST v2 uses the same DTOs as GraphQL (
common/dto/manager/v2/), providing a unified schema across both API surfaces.
REST v2 Handler → Adapter (api/adapters/) → Processor → Service → Repository
Key difference from v1: v2 handlers call shared Adapters instead of Processors directly. Adapters are shared with the GQL layer.
Key Files:
src/ai/backend/manager/api/rest/v2/{domain}/handler.py - Handlerssrc/ai/backend/manager/api/rest/v2/{domain}/registry.py - Route registrationsrc/ai/backend/manager/api/adapters/{domain}.py - Shared adapters (same as GQL)src/ai/backend/common/dto/manager/v2/{domain}/ - Shared DTOsfrom ai.backend.common.api_handlers import APIResponse, BodyParam
from ai.backend.common.dto.manager.v2.domain.request import AdminSearchDomainsInput
from ai.backend.manager.api.adapters.registry import Adapters
class DomainV2Handler:
def __init__(self, *, adapters: Adapters) -> None:
self._adapters = adapters
async def admin_search(
self,
body: BodyParam[AdminSearchDomainsInput],
) -> APIResponse:
payload = await self._adapters.domain.admin_search(body.parsed)
return APIResponse.build(
status_code=HTTPStatus.OK, response_model=payload,
)
Scope defines access boundaries.
Repository Scope:
src/ai/backend/manager/repositories/{domain}/types.pyrepositories/fair_share/types.py
DomainFairShareSearchScope, ProjectFairShareSearchScopePattern:
to_conditions() converts to query conditionsSee complete examples:
api/fair_share/handler.py - Scoped handler implementationsScope parameter is needed for:
search - Filter multiple items within scope
domain_search_users(scope: DomainScope, filter: ...)
batch operations - Process multiple items within scope
domain_batch_update_users(scope: DomainScope, ids: list[ID], ...)
Scope parameter is NOT needed for:
get - ID uniquely identifies item
user(id: ID) # ✅ No scope needed
# domain_user(scope: DomainScope, id: ID) # ❌ Unnecessary
update/delete/purge - ID uniquely identifies item
update_user(id: ID, data: ...) # ✅ No scope
delete_user(id: ID) # ✅ No scope
create - Scope info in data
create_user(data: CreateUserData) # data contains domain_name
Note: Scope prefix in API name (domain_search_users) is different from scope parameter (scope: DomainScope).
Filters convert API params to QueryCondition.
Adapter:
api/adapter.py - BaseFilterAdapterapi/fair_share/adapter.py - Domain adaptersPattern:
to_conditions(filters) → list[QueryCondition]to_orders(order_by) → list[QueryOrder]BatchQuerier → RepositorySee complete examples:
api/fair_share/adapter.py - Filter adaptersapi/fair_share/handler.py - Handler implementations with BatchQuerierAdmin operations require superadmin check before processing.
Pattern:
_check_superadmin(request) at handler startInsufficientPermission if not superadminSee complete examples:
api/rbac/handler.py - Admin handler implementationsREST uses offset-based pagination.
class PaginationQuery(BaseModel):
offset: int = 0
limit: int = 20
# Response
class SearchResult(BaseModel):
items: list[Item]
total_count: int
offset: int
limit: int
Client calculates: has_next = offset + limit < total_count
GraphQL Resolver → check_admin_only (if admin) → Adapter (info.context.adapters.*) → Processor → Service → Repository
Key Files:
src/ai/backend/manager/api/gql/{domain}/resolver/ - Resolverssrc/ai/backend/manager/api/gql/decorators.py - Custom decorators (required)src/ai/backend/manager/api/gql/pydantic_compat.py - PydanticNodeMixin, PydanticInputMixinsrc/ai/backend/manager/api/gql/utils.py - UtilitiesAll GQL types MUST use custom decorators from decorators.py — NEVER use @strawberry.type, @strawberry.input, or @strawberry.experimental.pydantic.* directly:
@gql_node_type(meta) — Relay Node types (inherit PydanticNodeMixin[DTO])@gql_pydantic_type(meta, model=DTO) — output types and payloads backed by a v2 Pydantic DTO@gql_pydantic_input(meta) — input types (inherit PydanticInputMixin[DTO])@gql_pydantic_interface(meta, model=DTO) — interface types@gql_connection_type(meta) — Connection[T] / Edge[T] subclasses@strawberry.enum, @strawberry.field, @strawberry.mutation, @strawberry.subscription may be used directly.
Strawberry Runtime Evaluation:
Naming Convention:
GQL suffix (DomainGQL, DomainScopeGQL, DomainFilterGQL)Scope vs Filter:
When a GQL node references another entity node, use strawberry.lazy() to avoid circular imports. Strawberry requires runtime type resolution, so TYPE_CHECKING imports alone are insufficient.
Pattern:
# 1. TYPE_CHECKING: for static analysis (mypy)
if TYPE_CHECKING:
from ai.backend.manager.api.gql.domain_v2.types.node import DomainV2GQL
# 2. Return type: Annotated with strawberry.lazy() for runtime resolution
# 3. Function body: runtime import + DataLoader
async def domain(self, info: Info[StrawberryGQLContext]) -> Annotated[
DomainV2GQL,
strawberry.lazy("ai.backend.manager.api.gql.domain_v2.types.node"),
]:
from ai.backend.manager.api.gql.domain_v2.types.node import DomainV2GQL
data = await info.context.data_loaders.domain_loader.load(self.domain_name)
return DomainV2GQL.from_data(data)
Optional return type: | None must be outside Annotated[]:
) -> Annotated[DomainV2GQL, strawberry.lazy("...")] | None: # ✅
) -> Annotated[DomainV2GQL | None, strawberry.lazy("...")]: # ❌ lazy cannot resolve union
DataLoaders (info.context.data_loaders): Use DataLoaders instead of individual fetch functions to prevent N+1 queries. See api/gql/data_loader/data_loaders.py for available loaders.
See examples:
api/gql/fair_share/types/domain.py - Cross-entity reference with DataLoaderapi/gql/domain_v2/types/node.py - DomainV2GQL with fair_shares/usage_bucketsapi/gql/fair_share/types/*.py - Scope and Filter patternsInput Types:
api/gql/types.py
ResourceGroupDomainScope, ResourceGroupProjectScope, ResourceGroupUserScopePattern:
@input maps to repository SearchScopeSee complete examples:
api/gql/types.py - Scope input typesapi/gql/fair_share/resolver/domain.py - Resolver implementationsPattern:
check_admin_only(info) at resolver startInsufficientPermission if not superadminSee complete examples:
api/gql/utils.py - check_admin_only() utilityapi/gql/*/resolver/*.py - Admin resolver implementationsGraphQL supports cursor-based (Relay spec).
@strawberry.type
class PageInfo:
has_next_page: bool
has_previous_page: bool
start_cursor: str | None
end_cursor: str | None
@strawberry.type
class UserConnection:
edges: list[UserEdge]
page_info: PageInfo
total_count: int
See: api/gql/ for cursor pagination examples
common/dto/manager/. Per-domain adapters in api/rest/{domain}/adapter.py.common/dto/manager/v2/. Shared adapters in api/adapters/{domain}.py.common/dto/manager/v2/. Strawberry types wrap DTOs via custom decorators.superadmin_required middleware, GQL uses check_admin_only(info).domain_create_user, GQL uses domainCreateUser.When implementing REST API, also implement:
✅ SDK Function (client/func/{domain}.py)
@api_function decorator✅ CLI Command (client/cli/admin/{domain}.py)
Integration flow:
CLI → SDK → REST API → Processor → Service → Repository
See examples:
src/ai/backend/client/func/admin.py - SDKsrc/ai/backend/client/cli/admin/user.py - CLISee: /tdd-guide skill and tests/CLAUDE.md for complete testing strategies.
Test hierarchy:
Repository Tests → Real DB (with_tables)
Service Tests → Mock repositories
API Handler Tests → Mock processors
CLI Tests → Mock HTTP
When implementing new API:
✅ Repository (/repository-guide)
✅ Service (/service-guide)
✅ REST API v2 (all new endpoints)
common/dto/manager/v2/ DTOsBodyParam[DTO] input, APIResponse.build(response_model=payload) output✅ GraphQL (optional)
@gql_pydantic_type / @gql_pydantic_input with DTO — no @strawberry.type directlyinfo.context.adapters.*)✅ Client SDK
@api_function decorator✅ CLI
✅ Tests
/service-guide - Actions, Processors/repository-guide - Data access/tdd-guide - TDD workflowsrc/ai/backend/manager/api/README.mdREST API:
src/ai/backend/manager/api/fair_share/handler.pysrc/ai/backend/manager/api/rbac/handler.pyGraphQL:
src/ai/backend/manager/api/gql/fair_share/resolver/domain.pysrc/ai/backend/manager/api/gql/types.pyClient SDK/CLI:
src/ai/backend/client/func/admin.pysrc/ai/backend/client/cli/admin/user.pyStandard operations:
Scope prefix (API only):
{scope}_operation (domain_create_user)admin_operation (admin_create_domain)Key patterns:
Integration:
Next steps:
/repository-guide)/service-guide)/tdd-guide)