Skip to main content

GraphQL SDL generation and query optimization for SQLModel

Project description

nexusx

Write SQLModel classes. Get a complete API.

pypi PyPI Downloads

Most Python backends split one domain across five files — SQLModel table, Pydantic DTO, GraphQL resolver with hand-wired DataLoader, REST handler, MCP tool definition. Change a field, sync five files. nexusx collapses them: declare entities once, get back GraphQL with auto-batched relations, typed REST + OpenAPI, and a 4-layer MCP server — all from the same SQLModel classes.

flowchart LR
    sqlmodel["SQLModel"]

    sqlmodel --> graphql["GraphQL"]
    graphql --> mcp1["MCP"]

    sqlmodel --> usecase["UseCaseService"]
    usecase --> rest["REST"]
    usecase --> mcp2["MCP"]
    usecase --> cli["CLI"]

Install

pip install nexusx
pip install nexusx[fastmcp]  # with MCP support

Requires Python ≥ 3.10.

Features

  • N+1-proof by default — DataLoaders are auto-generated from SQLAlchemy metadata. Querying users { posts { comments } } is three SQL round-trips total, not thousands.
  • Selection runs through the stack — GraphQL field sets → DefineSubset DTO → SQL load_only. A 50-column table queried for 3 columns reads 3 columns from disk.
  • Relationships beyond ORMRelationship(...) is a first-class escape hatch: Redis caches, Elasticsearch, or external APIs flow through the same DataLoader / DTO / ER-diagram plumbing as native SQLAlchemy relations.
  • One service, many transports — a UseCaseService method generates GraphQL / FastAPI routes / MCP tools / CLI commands, with types and docs derived from the Python signature.
  • AI-agent-first MCP — the UseCase path rejects GraphQL introspection (often 50K+ tokens on real schemas) and exposes compact describe_* discovery tools instead.

Quick Start

Step 1 — Entities + GraphQL

Define SQLModel entities and decorate entry-point methods with @query. GraphQLHandler walks the entity graph, generates SDL, and resolves relationships through DataLoader — one batched SQL per relationship level instead of N+1.

from sqlmodel import SQLModel, Field, Relationship, select
from nexusx import query, mutation, GraphQLHandler

class User(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str
    posts: list["Post"] = Relationship(back_populates="author")

    @query
    async def get_users(cls, limit: int = 10) -> list["User"]:
        async with get_session() as session:
            return (await session.exec(select(cls).limit(limit))).all()

class Post(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    title: str
    author_id: int = Field(foreign_key="user.id")
    author: User | None = Relationship(back_populates="posts")

handler = GraphQLHandler(base=SQLModel, session_factory=async_session)

User.get_users becomes a GraphQL query field. Querying { userGetUsers(limit: 5) { name posts { title } } } triggers exactly two SQL round-trips — one for the users, one batched SELECT ... WHERE author_id IN (...) for all their posts. Scale to 100 users with 10 posts each and the answer is still two queries, not 101 — the same shape extends to arbitrary nesting depth. The handler is executor-only: mount it on any ASGI app via a POST route that calls handler.execute(query=...) (see demo/blog/app.py for a complete FastAPI example with GraphiQL). handler.get_sdl() returns the schema for codegen or external clients.

The GraphQL mode guide covers filters, pagination (enable_pagination=True wraps lists in Result { items, pagination }), and AutoQueryConfig for auto-generated by_id / by_filter queries across every entity.

Step 2 — Typed REST with DTOs

GraphQL exposes entities directly. For REST handlers or service-layer code you usually want a smaller, intentional shape per endpoint — that's DefineSubset. Declare which fields to keep; relationship fields auto-load when their name matches an ORM relationship, so author: UserDTO | None = None populates itself from the underlying author_id FK without any loader boilerplate.

from nexusx import DefineSubset, ErManager

class UserDTO(DefineSubset):
    __subset__ = (User, ("id", "name"))

class PostDTO(DefineSubset):
    __subset__ = (Post, ("id", "title", "author_id"))
    author: UserDTO | None = None  # auto-loaded — field name matches relationship

Resolver = ErManager(base=SQLModel, session_factory=async_session).create_resolver()
dtos = await Resolver().resolve(posts)

posts is whatever list of ORM instances you fetched — your query, your filter, your permissions. The Resolver traverses the DTO tree level-by-level, batching each level's loads the same way GraphQL does, and returns typed PostDTO instances with relationships filled in. Add resolve_* methods to override the auto-loader for a field, post_* methods for derived/computed fields. See the Core API guide.

Step 3 — MCP + REST from business logic

For operations that compose multiple entities, apply permissions, or go beyond single-table CRUD, write a UseCaseService — a plain class whose @query / @mutation methods hold your business logic. One service class generates both an MCP server (4-layer progressive disclosure for AI agents) and FastAPI routes (one POST per method, types derived from signatures, OpenAPI docs included).

from nexusx import UseCaseService, UseCaseAppConfig, create_use_case_graphql_mcp_server, create_use_case_router

class SprintService(UseCaseService):
    @query
    async def list_sprints(cls) -> list[SprintSummary]:
        """Get all sprints with task counts."""
        ...

config = UseCaseAppConfig(name="project", services=[SprintService])

# MCP for AI agents
mcp = create_use_case_graphql_mcp_server(apps=[config])
mcp.run()

# REST for frontend
app.include_router(create_use_case_router(config))

Methods are regular async functions — they can call Resolver().resolve(...) from Step 2 internally, so business logic and DTO assembly compose freely. Same Python class, three surfaces (MCP / REST / GraphQL-via-MCP). See feature highlights for the full picture.

How It Compares

nexusx Strawberry FastAPI + SQLModel FastMCP
GraphQL auto-gen
REST + OpenAPI ✓ (one handler per endpoint)
MCP
N+1 prevention ✓ auto from metadata manual DataLoader per relation
Relationship auto-loading ✓ via SQLAlchemy inspect hand-written resolver per relation
SQL column pruning follows selection
Same code → GraphQL + REST + MCP

Strawberry and FastMCP are excellent at what they do — Strawberry gives you fine-grained resolver control for complex GraphQL APIs, and FastMCP is the cleanest way to expose a single Python function as an MCP tool. nexusx trades per-endpoint control for cross-protocol consistency: the tradeoff pays off when one codebase needs to serve several transports at once.

Status

Alpha — actively developed, APIs may change between minor versions. Not yet battle-tested in production. Bug reports and PRs welcome.

Demos

git clone https://github.com/allmonday/nexusx.git && cd nexusx && bash start_all.sh
Port Mode
8000 GraphQL playground
8001 Core API (REST + DTOs)
8005 Paginated GraphQL
8006 UseCase MCP (4-layer)
8007 UseCase FastAPI (REST)
8008 Voyager visualization

AI Agent Skill

A 4-phase skill guides AI coding agents: clarify requirements → build POC → add queries → productize.

ln -s $(pwd)/skill ~/.claude/skills/nexusx-4phase

Development

./scripts/check-ci.sh       # lint + type-check + tests
uv run pytest               # tests only
uv run ruff check src/ tests/  # lint only
uv run mypy src/            # type-check only

Documentation

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nexusx-3.4.2.tar.gz (1.5 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nexusx-3.4.2-py3-none-any.whl (728.8 kB view details)

Uploaded Python 3

File details

Details for the file nexusx-3.4.2.tar.gz.

File metadata

  • Download URL: nexusx-3.4.2.tar.gz
  • Upload date:
  • Size: 1.5 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for nexusx-3.4.2.tar.gz
Algorithm Hash digest
SHA256 ed8b320e476a2565182dbd6d4a9f43cca6892222e341579deef5599a7220dc06
MD5 79d411551a7d6698f8ea85842229f635
BLAKE2b-256 71c3f1ceac015b94361afea1c01e55c5ca83adb7e6dbefba6f8483686b26fe25

See more details on using hashes here.

File details

Details for the file nexusx-3.4.2-py3-none-any.whl.

File metadata

  • Download URL: nexusx-3.4.2-py3-none-any.whl
  • Upload date:
  • Size: 728.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for nexusx-3.4.2-py3-none-any.whl
Algorithm Hash digest
SHA256 a1d8b6d084f2108721d0d28d7535a2c33b355fa78bb4c56d2409c5fa253f1e4e
MD5 49df9f3e2197fb90e3e408157197774c
BLAKE2b-256 c25fae7ec8ebcaad418fd15a0d56757863c107d07f92ccd2b3a186a3a4ae9137

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page