From galaxy-dev
Create Galaxy REST API endpoints with FastAPI routers, Pydantic schemas, and manager pattern. Use for: new API routes, FastAPI endpoints, REST resources, Pydantic request/response models, lib/galaxy/webapps/galaxy/api routers, lib/galaxy/schema definitions, API controller creation.
How this skill is triggered — by the user, by Claude, or both
Slash command
/galaxy-dev:galaxy-api-endpointThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Persona: You are a senior Galaxy backend developer specializing in FastAPI and the manager pattern.
Persona: You are a senior Galaxy backend developer specializing in FastAPI and the manager pattern.
Arguments:
This guide walks you through creating a new REST API endpoint following Galaxy's architecture patterns.
If $ARGUMENTS is empty, ask the user:
Use their answers to guide the rest of the workflow.
Before starting, find the most recent similar endpoint to use as a pattern:
# Find recently modified API routers
ls -t lib/galaxy/webapps/galaxy/api/*.py | head -5
Read one of these files to understand current patterns. Good examples:
lib/galaxy/webapps/galaxy/api/job_files.py - Simple CRUD operationslib/galaxy/webapps/galaxy/api/workflows.py - Complex resource with many operationslib/galaxy/webapps/galaxy/api/histories.py - RESTful resource with nested routesKey patterns to observe:
router = APIRouter(tags=["resource_name"])DependsOnTrans, custom dependency functionsgalaxy.schemaCreate request/response models in lib/galaxy/schema/ (or update existing schema file if one exists for this domain).
Location: lib/galaxy/schema/schema.py (or domain-specific file like lib/galaxy/schema/workflows.py)
Common imports:
from typing import Optional, List
from pydantic import Field
from galaxy.schema.fields import EncodedDatabaseIdField
from galaxy.schema.schema import Model
Example schema definitions:
class MyResourceCreateRequest(Model):
"""Request model for creating a new resource."""
name: str = Field(..., description="Resource name")
description: Optional[str] = Field(None, description="Optional description")
class MyResourceResponse(Model):
"""Response model for resource operations."""
id: EncodedDatabaseIdField = Field(..., description="Encoded resource ID")
name: str
description: Optional[str]
create_time: datetime
update_time: datetime
class MyResourceListResponse(Model):
"""Response model for listing resources."""
items: List[MyResourceResponse]
total_count: int
Field types commonly used:
EncodedDatabaseIdField - For Galaxy's encoded IDsDecodedDatabaseIdField - For decoded integer IDs (internal use)str, int, bool, float - Standard typesOptional[T] - For nullable fieldsList[T] - For arraysdatetime - For timestampsBest practices:
description to all fields for OpenAPI documentation... for required fields, defaults for optionalField(alias="...") if API name differs from Python nameBusiness logic belongs in manager classes in lib/galaxy/managers/.
Location:
lib/galaxy/managers/<resource>s.pylib/galaxy/managers/<resource>s.pyManager pattern structure:
from typing import Optional
from galaxy import model
from galaxy.managers.context import ProvidesUserContext
from galaxy.model import Session
class MyResourceManager:
"""Manager for MyResource operations."""
def __init__(self, app):
self.app = app
self.sa_session: Session = app.model.context
def create(
self,
trans: ProvidesUserContext,
name: str,
description: Optional[str] = None
) -> model.MyResource:
"""Create a new resource."""
resource = model.MyResource(
user=trans.user,
name=name,
description=description
)
self.sa_session.add(resource)
self.sa_session.flush()
return resource
def get(self, trans: ProvidesUserContext, resource_id: int) -> model.MyResource:
"""Get resource by ID."""
resource = self.sa_session.get(model.MyResource, resource_id)
if not resource:
raise exceptions.ObjectNotFound("Resource not found")
if not self.is_accessible(resource, trans.user):
raise exceptions.ItemAccessibilityException("Access denied")
return resource
def is_accessible(self, resource: model.MyResource, user: Optional[model.User]) -> bool:
"""Check if user can access this resource."""
if not user:
return False
return resource.user_id == user.id
def list_for_user(self, trans: ProvidesUserContext) -> List[model.MyResource]:
"""List all resources for the current user."""
stmt = select(model.MyResource).where(
model.MyResource.user_id == trans.user.id
)
return self.sa_session.scalars(stmt).all()
Manager best practices:
app (the Galaxy application object)trans (transaction/request context) as first parameterself.sa_session for database operationsgalaxy.exceptionsselect() syntax for queriesCreate or update the API router in lib/galaxy/webapps/galaxy/api/.
Location: lib/galaxy/webapps/galaxy/api/<resource>s.py
Router template:
"""
API endpoints for MyResource operations.
"""
import logging
from typing import Optional
from fastapi import (
APIRouter,
Depends,
Path,
Query,
status,
)
from galaxy.managers.context import ProvidesUserContext
from galaxy.managers.myresources import MyResourceManager
from galaxy.schema.schema import (
MyResourceCreateRequest,
MyResourceResponse,
MyResourceListResponse,
)
from galaxy.webapps.galaxy.api import (
DependsOnTrans,
Router,
)
from galaxy.webapps.galaxy.api.depends import get_app
log = logging.getLogger(__name__)
router = Router(tags=["myresources"])
# Dependency for manager
def get_myresource_manager(app=Depends(get_app)) -> MyResourceManager:
return MyResourceManager(app)
@router.cbv
class FastAPIMyResources:
manager: MyResourceManager = Depends(get_myresource_manager)
@router.get(
"/api/myresources",
summary="List all resources for current user",
response_model=MyResourceListResponse,
)
def index(
self,
trans: ProvidesUserContext = DependsOnTrans,
) -> MyResourceListResponse:
"""List all resources owned by the current user."""
items = self.manager.list_for_user(trans)
return MyResourceListResponse(
items=[self._serialize(item) for item in items],
total_count=len(items),
)
@router.post(
"/api/myresources",
summary="Create a new resource",
status_code=status.HTTP_201_CREATED,
response_model=MyResourceResponse,
)
def create(
self,
trans: ProvidesUserContext = DependsOnTrans,
request: MyResourceCreateRequest = ...,
) -> MyResourceResponse:
"""Create a new resource."""
resource = self.manager.create(
trans,
name=request.name,
description=request.description,
)
return self._serialize(resource)
@router.get(
"/api/myresources/{id}",
summary="Get resource by ID",
response_model=MyResourceResponse,
)
def show(
self,
trans: ProvidesUserContext = DependsOnTrans,
id: EncodedDatabaseIdField = Path(..., description="Resource ID"),
) -> MyResourceResponse:
"""Get a specific resource by ID."""
decoded_id = trans.security.decode_id(id)
resource = self.manager.get(trans, decoded_id)
return self._serialize(resource)
def _serialize(self, resource) -> MyResourceResponse:
"""Convert model object to response schema."""
return MyResourceResponse(
id=trans.security.encode_id(resource.id),
name=resource.name,
description=resource.description,
create_time=resource.create_time,
update_time=resource.update_time,
)
Router best practices:
Router (capital R) from galaxy.webapps.galaxy.api (subclass of FastAPI's APIRouter)@router.cbv class-based views for grouping related endpointsmanager: Manager = Depends(get_manager)DependsOnTrans for transaction contextPath(...) with descriptionsQuery(...) with defaultsstatus_code=status.HTTP_201_CREATED for creates)summary to all endpoints for OpenAPI docsThe router must be registered in the main application builder.
Location: lib/galaxy/webapps/galaxy/buildapp.py
Add import:
from galaxy.webapps.galaxy.api import myresources
Register router in app_factory():
app.include_router(myresources.router)
Find the section: Look for other app.include_router() calls and add yours in alphabetical order.
Create tests in lib/galaxy_test/api/.
Location: lib/galaxy_test/api/test_<resource>s.py
Test template:
"""
API tests for MyResource endpoints.
"""
from galaxy_test.base.populators import DatasetPopulator
from ._framework import ApiTestCase
class TestMyResourcesApi(ApiTestCase):
"""Tests for /api/myresources endpoints."""
def setUp(self):
super().setUp()
self.dataset_populator = DatasetPopulator(self.galaxy_interactor)
def test_create_myresource(self):
"""Test creating a new resource."""
payload = {
"name": "Test Resource",
"description": "Test description",
}
response = self._post("myresources", data=payload, json=True)
self._assert_status_code_is(response, 201)
resource = response.json()
self._assert_has_keys(resource, "id", "name", "description", "create_time")
assert resource["name"] == "Test Resource"
def test_list_myresources(self):
"""Test listing resources."""
# Create some test data
self._create_myresource("Resource 1")
self._create_myresource("Resource 2")
# List resources
response = self._get("myresources")
self._assert_status_code_is_ok(response)
data = response.json()
assert data["total_count"] >= 2
assert len(data["items"]) >= 2
def test_get_myresource(self):
"""Test getting a specific resource."""
resource_id = self._create_myresource("Test Resource")
response = self._get(f"myresources/{resource_id}")
self._assert_status_code_is_ok(response)
resource = response.json()
assert resource["id"] == resource_id
assert resource["name"] == "Test Resource"
def test_get_nonexistent_myresource(self):
"""Test getting a resource that doesn't exist."""
response = self._get("myresources/invalid_id")
self._assert_status_code_is(response, 404)
def test_create_myresource_as_different_user(self):
"""Test that users can only see their own resources."""
# Create as first user
resource_id = self._create_myresource("User 1 Resource")
# Switch to different user
with self._different_user():
# Should not be able to access
response = self._get(f"myresources/{resource_id}")
self._assert_status_code_is(response, 403)
def _create_myresource(self, name: str) -> str:
"""Helper to create a resource and return its ID."""
payload = {"name": name, "description": f"Description for {name}"}
response = self._post("myresources", data=payload, json=True)
self._assert_status_code_is(response, 201)
return response.json()["id"]
Test patterns:
ApiTestCase from lib/galaxy_test/api/_framework.pyself._get(), self._post(), self._put(), self._delete() (paths relative to /api/)self._assert_status_code_is(response, 200) for status checksself._assert_status_code_is_ok(response) for 2xx statusself._assert_has_keys(obj, "key1", "key2") to verify response structureself._different_user() context manager to test as different user_create_myresource() for test data setupRun your new tests using the Galaxy test runner:
# Run all tests for your new API
./run_tests.sh -api lib/galaxy_test/api/test_myresources.py
# Run a specific test
./run_tests.sh -api lib/galaxy_test/api/test_myresources.py::TestMyResourcesApi::test_create_myresource
# Run with verbose output
./run_tests.sh -api lib/galaxy_test/api/test_myresources.py --verbose_errors
IMPORTANT: Always use ./run_tests.sh, not pytest directly. The wrapper script sets up the correct environment.
Start Galaxy dev server:
./run.sh
Check OpenAPI docs:
Navigate to http://localhost:8080/api/docs and verify your endpoints appear
Manual test with curl:
# Create
curl -X POST http://localhost:8080/api/myresources \
-H "Content-Type: application/json" \
-d '{"name": "Test", "description": "Test resource"}'
# List
curl http://localhost:8080/api/myresources
# Get specific
curl http://localhost:8080/api/myresources/{id}
Check auto-generated TypeScript types:
The frontend types in client/src/api/schema/schema.ts will be auto-generated from your Pydantic schemas next time the schema is rebuilt.
When implementing your endpoint, reference these files:
Recent API examples:
ls -t lib/galaxy/webapps/galaxy/api/*.py | head -5
Schema patterns:
lib/galaxy/schema/schema.py - Main schema definitionslib/galaxy/schema/fields.py - Custom field typesManager patterns:
ls lib/galaxy/managers/*.py
Test examples:
ls lib/galaxy_test/api/test_*.py
trans.security.encode_id()) and decode in endpointstrans as first parameterself.sa_session.flush() after adding objects, not commit()galaxy.exceptions, not generic onesbuildapp.py./run_tests.sh -api, not plain pytestAfter creating your endpoint:
/api/docsFor more details, see:
reference.md in this skill directory for concrete code examplesnpx claudepluginhub arash77/galaxy-claude-marketplace --plugin galaxy-devREST API design and implementation patterns for FastAPI endpoints including CRUD operations, pagination, filtering, error handling, and request/response models. Use when building FastAPI endpoints, creating REST APIs, implementing CRUD operations, adding pagination, designing API routes, handling API errors, or when user mentions FastAPI patterns, REST API design, endpoint structure, API best practices, or HTTP endpoints.
Creates FastAPI endpoints with layered architecture (Router → Service → Repository), Pydantic schemas, SQLAlchemy models, async CRUD, soft deletes, and tests. Use for new API endpoints, CRUD ops, or domain scaffolding.
Creates FastAPI routers with CRUD operations, authentication dependencies, Pydantic response models, and HTTP status codes for REST APIs.