api-guide

star 646

Guide for implementing REST and GraphQL APIs (create, get, search, update, delete, purge, scope prefix patterns, admin_ prefix, SearchScope, BaseFilterAdapter, @api_function, Click CLI)

lablup By lablup schedule Updated 6/5/2026

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

API Implementation Guide

Guide for implementing REST and GraphQL APIs with standard operations and scope patterns.

Standard Operations

APIs implement 6 standard operations:

  1. create - Create new entity
  2. get - Retrieve single entity
  3. search - Query with filters and pagination
  4. update - Update entity
  5. delete - Delete entity (soft)
  6. purge - Permanently remove entity (hard)

Multi-target: batch_* (atomic, one statement) vs bulk_* (per-row, partial failures). See /repository-guide.

Scope Prefix Rules

API layer only (Service/Repository layers don't use prefix)

Scoped Operations

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: ...)

Admin Operations (No Scope)

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: ...)

When to Separate admin_ vs Non-admin Endpoints

search — always two variants:

  • admin_search_*: superadmin, no scope — queries entire system.
  • {scope}_search_*: non-admin, scope required — queries within the given scope only.
  • There is NO "search everything without scope" for non-admin users.

create / update / get / delete / purge — depends on the entity:

  1. Admin-only entity (e.g., Domain, ContainerRegistry): single admin_ endpoint.
  2. Both admin and users, but behavior differs (e.g., admin can set more fields on create/update): separate admin_ and non-admin endpoints with different DTOs.
  3. Both admin and users, only permission check differs: single endpoint — admin already has entity access permissions, no need for a separate admin_ variant.

REST v1 API Patterns (Legacy)

New endpoints MUST use REST v2 patterns below. REST v1 is for existing legacy handlers only — do not add new handlers here.

Architecture

REST v1 Handler → Processor → Service → Repository

Key Files:

  • src/ai/backend/manager/api/rest/{domain}/handler.py - Handlers
  • src/ai/backend/manager/api/rest/{domain}/adapter.py - Filters (per-domain)
  • src/ai/backend/manager/api/rest/adapter.py - Base adapter

See complete examples:

  • api/rest/fair_share/handler.py - Handler calling processor
  • services/processors.py - ActionProcessor base class

REST v2 API Patterns

REST v2 uses the same DTOs as GraphQL (common/dto/manager/v2/), providing a unified schema across both API surfaces.

Architecture

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 - Handlers
  • src/ai/backend/manager/api/rest/v2/{domain}/registry.py - Route registration
  • src/ai/backend/manager/api/adapters/{domain}.py - Shared adapters (same as GQL)
  • src/ai/backend/common/dto/manager/v2/{domain}/ - Shared DTOs

Handler Pattern

from 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 Pattern

Scope defines access boundaries.

Repository Scope:

  • src/ai/backend/manager/repositories/{domain}/types.py
  • Example: repositories/fair_share/types.py
    • DomainFairShareSearchScope, ProjectFairShareSearchScope

Pattern:

  • Frozen dataclass with scope params
  • to_conditions() converts to query conditions

See complete examples:

  • api/fair_share/handler.py - Scoped handler implementations

Scope Parameter Usage

Scope parameter is needed for:

  1. search - Filter multiple items within scope

    domain_search_users(scope: DomainScope, filter: ...)
    
  2. batch operations - Process multiple items within scope

    domain_batch_update_users(scope: DomainScope, ids: list[ID], ...)
    

Scope parameter is NOT needed for:

  1. get - ID uniquely identifies item

    user(id: ID)  # ✅ No scope needed
    # domain_user(scope: DomainScope, id: ID)  # ❌ Unnecessary
    
  2. update/delete/purge - ID uniquely identifies item

    update_user(id: ID, data: ...)  # ✅ No scope
    delete_user(id: ID)  # ✅ No scope
    
  3. 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).

Filter Pattern

Filters convert API params to QueryCondition.

Adapter:

  • api/adapter.py - BaseFilterAdapter
  • api/fair_share/adapter.py - Domain adapters

Pattern:

  • to_conditions(filters)list[QueryCondition]
  • to_orders(order_by)list[QueryOrder]
  • Adapter → BatchQuerier → Repository

See complete examples:

  • api/fair_share/adapter.py - Filter adapters
  • api/fair_share/handler.py - Handler implementations with BatchQuerier

Admin_ Prefix Pattern

Admin operations require superadmin check before processing.

Pattern:

  • _check_superadmin(request) at handler start
  • Raise InsufficientPermission if not superadmin

See complete examples:

  • api/rbac/handler.py - Admin handler implementations

Pagination

REST 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 Patterns

Architecture

GraphQL Resolver → check_admin_only (if admin) → Adapter (info.context.adapters.*) → Processor → Service → Repository

Key Files:

  • src/ai/backend/manager/api/gql/{domain}/resolver/ - Resolvers
  • src/ai/backend/manager/api/gql/decorators.py - Custom decorators (required)
  • src/ai/backend/manager/api/gql/pydantic_compat.py - PydanticNodeMixin, PydanticInputMixin
  • src/ai/backend/manager/api/gql/utils.py - Utilities

Decorator Rules

All 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.

Type System Rules

Strawberry Runtime Evaluation:

  • Strawberry types are evaluated at runtime
  • NEVER use TYPE_CHECKING for Strawberry types (Connection, Filter, OrderBy, Input, Type)
  • ALWAYS import Strawberry types directly at module level
  • Only use TYPE_CHECKING for data layer types not used by Strawberry
  • If lazy import needed: use strawberry.lazy() or string-based forward references

Naming Convention:

  • All GraphQL types MUST have GQL suffix (DomainGQL, DomainScopeGQL, DomainFilterGQL)
  • Distinguishes GraphQL types from data layer types

Scope vs Filter:

  • Scope: Required context parameters (resource_group, domain_name, project_id)
  • Filter: Optional filtering conditions (name contains, status equals, created after)
  • NEVER put optional fields in Scope - use Filter instead
  • Scope fields must all be required (no default values, no Optional types)

Cross-Entity Reference Resolvers

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 DataLoader
  • api/gql/domain_v2/types/node.py - DomainV2GQL with fair_shares/usage_buckets
  • api/gql/fair_share/types/*.py - Scope and Filter patterns

Scope Pattern

Input Types:

  • api/gql/types.py
    • ResourceGroupDomainScope, ResourceGroupProjectScope, ResourceGroupUserScope

Pattern:

  • Strawberry @input maps to repository SearchScope
  • GraphQL input → Repository scope type conversion

See complete examples:

  • api/gql/types.py - Scope input types
  • api/gql/fair_share/resolver/domain.py - Resolver implementations

Admin Check

Pattern:

  • check_admin_only(info) at resolver start
  • Raise InsufficientPermission if not superadmin

See complete examples:

  • api/gql/utils.py - check_admin_only() utility
  • api/gql/*/resolver/*.py - Admin resolver implementations

Pagination

GraphQL 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

REST v1 vs REST v2 vs GraphQL

  • REST v1 (legacy): Handler → Processor. DTOs from common/dto/manager/. Per-domain adapters in api/rest/{domain}/adapter.py.
  • REST v2: Handler → Adapter. DTOs from common/dto/manager/v2/. Shared adapters in api/adapters/{domain}.py.
  • GraphQL: Resolver → Adapter (same adapters as REST v2). DTOs from common/dto/manager/v2/. Strawberry types wrap DTOs via custom decorators.
  • Admin check: REST uses superadmin_required middleware, GQL uses check_admin_only(info).
  • Naming: REST uses domain_create_user, GQL uses domainCreateUser.

Client SDK + CLI Integration

When implementing REST API, also implement:

  1. SDK Function (client/func/{domain}.py)

    • Use @api_function decorator
    • Map to REST endpoint
  2. CLI Command (client/cli/admin/{domain}.py)

    • Click command
    • Calls SDK function

Integration flow:

CLI → SDK → REST API → Processor → Service → Repository

See examples:

  • src/ai/backend/client/func/admin.py - SDK
  • src/ai/backend/client/cli/admin/user.py - CLI

Testing

See: /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

Implementation Checklist

When implementing new API:

  1. Repository (/repository-guide)

    • Implement standard operations
    • Define SearchScope
  2. Service (/service-guide)

    • Define Actions/ActionResults
    • Implement service methods
    • Create processors
  3. REST API v2 (all new endpoints)

    • Handler calls Adapter, uses common/dto/manager/v2/ DTOs
    • BodyParam[DTO] input, APIResponse.build(response_model=payload) output
    • Admin check and scope prefix
  4. GraphQL (optional)

    • @gql_pydantic_type / @gql_pydantic_input with DTO — no @strawberry.type directly
    • Resolver calls Adapter (info.context.adapters.*)
    • Admin check if needed
  5. Client SDK

    • Add SDK function
    • @api_function decorator
  6. CLI

    • Click command
    • Integrate with SDK
  7. Tests

    • Repository (real DB)
    • Service (mock repo)
    • Handler (mock processors)
    • CLI (mock HTTP)

Related Documentation

  • Service Layer: /service-guide - Actions, Processors
  • Repository Layer: /repository-guide - Data access
  • Testing: /tdd-guide - TDD workflow
  • API README: src/ai/backend/manager/api/README.md

Examples

REST API:

  • src/ai/backend/manager/api/fair_share/handler.py
  • src/ai/backend/manager/api/rbac/handler.py

GraphQL:

  • src/ai/backend/manager/api/gql/fair_share/resolver/domain.py
  • src/ai/backend/manager/api/gql/types.py

Client SDK/CLI:

  • src/ai/backend/client/func/admin.py
  • src/ai/backend/client/cli/admin/user.py

Summary

Standard operations:

  • create, get, search, update, delete, purge
  • batch_update, batch_delete, batch_purge

Scope prefix (API only):

  • Scoped: {scope}_operation (domain_create_user)
  • Admin: admin_operation (admin_create_domain)

Key patterns:

  • Scope → SearchScope → QueryCondition
  • Filter → Adapter → QueryCondition
  • Admin → check permission first

Integration:

  • REST API + SDK + CLI (unified stack)
  • GraphQL (separate, optional)

Next steps:

  1. Implement repository (/repository-guide)
  2. Implement service (/service-guide)
  3. Implement API handlers
  4. Add SDK + CLI
  5. Write tests (/tdd-guide)
Install via CLI
npx skills add https://github.com/lablup/backend.ai --skill api-guide
Repository Details
star Stars 646
call_split Forks 179
navigation Branch main
article Path SKILL.md
More from Creator