Skip to main content

nyxa

Batteries-included DX for Python API backends on FastAPI.

Nyxa is pronounced Nik-sah (/ˈnɪksə/). Site: nyxadev.com · Repo: NyxaDev/nyxa.

Nyxa gives you a convention-based CLI (nyxa, with an artisan alias), project layout, central routes.py + controllers, and small runtime bases (AppError, abort helpers, register, events). It is backend/API only — no server-side templates, Vite, or frontend stack.

Long-term plans: see the workspace roadmap/.


Install

uv add nyxa
# or
pip install nyxa

The import and CLI stay nyxa:

from nyxa import AppError, Route, register

Quick start

uvx --from nyxa nyxa new myapp
cd myapp
uv sync
uv run dev

Interactive mode (TTY) prompts for database, auth, and optional packs. For CI:

nyxa new myapp --database sqlite --auth none --no-input

Cheat sheet: docs/cheat-sheet.md. Adapter upgrades: docs/upgrading.md.

CORS and logging

Optional; off by default. Configure in nyxa.toml or env — applied inside register():

[cors]
origins = ["http://localhost:3000"]

[logging]
json = true
level = "INFO"
CORS_ORIGINS=http://localhost:3000
NYXA_LOG_JSON=1

Or call configure_cors(app, origins=[...]) / configure_logging() manually.

Scaffold into the current directory:

mkdir myapp && cd myapp
uvx --from nyxa nyxa init

Monorepo / unpublished local checkout:

uv run nyxa new myapp --nyxa-path /path/to/packages/nyxa

Routing

Define routes in src/{app}/routes.py. Nyxa compiles them at startup into normal FastAPI endpoints (no Route stack on the request hot path).

What you need Nyxa
Map a path to a controller action Route.get("/users", [UserController, "index"])
Group routes under a prefix with Route.prefix("/api"):
Name a route .name("users.index")
RESTful resource routes Route.resource("posts", PostController)
List compiled routes nyxa route:list
# src/myapp/routes.py
from nyxa.routing import Route
from myapp.http.controllers.user_controller import UserController

with Route.prefix("/api"):
    Route.resource("users", UserController)

Stable CLI (0.1)

Entry points: nyxa, artisan (alias), and dev.

Generated scaffolds include Google-style docstrings (Args / Returns) on modules, classes, and functions.

Command Purpose
nyxa new / nyxa init [name] Scaffold a new API app (--database, --auth, --no-input, --queue/--cache/--mail/--storage, --force, --nyxa-path)
nyxa make:controller Controller + auto route; shorts -a/-m/-r/-R/-e/-s
nyxa make:exc Error + handler; nested paths (user/NotFound); status heuristic + --status
nyxa make:req / make:res FormRequest / response models
nyxa make:policy Policy class (nyxa-auth)
nyxa make:middleware Middleware class
nyxa make:provider Service provider (register / boot)
nyxa make:service Service class
nyxa make:dependency FastAPI dependency
nyxa make:schema / make:enum / make:config Schema, enum, settings
nyxa make:event / make:listener / make:job Events, listeners (--event required), jobs (Job class when nyxa-queue installed)
nyxa queue work Run a job once in-process (nyxa-queue)
nyxa cache clear Flush cache store (nyxa-cache)
nyxa mail test Send sample mail via configured mailer (nyxa-mail)
nyxa make:test Pytest stub
nyxa make:resource Full CRUD slice + Route.resource stub
nyxa route:list / list:routes List routes (method, URI, name, action)
dev Run uvicorn using nyxa.toml [dev]

Stable public API (0.1)

Supported imports for application code:

from nyxa import (
    AppError,
    Controller,
    EventDispatcher,
    ForbiddenError,
    FormRequest,
    Route,
    ServiceProvider,
    abort,
    bind,
    check,
    config,
    context,
    current_user,
    dispatcher,
    env,
    guest,
    no_content,
    not_found,
    register,
    route,
    settings,
    success,
    user,
)
Symbol Role
AppError Base application error (override via [bases].error); default JSON handler on register
UnauthorizedError / ForbiddenError / NotFoundError / … Core HTTP-mapped AppError subclasses (401–429)
abort / unauthorized / forbidden / not_found / … Raise helpers (NoReturn) for common exits
success / created / accepted / no_content / json_response JSON envelope / empty-body helpers
context / ContextMiddleware Request-scoped key/value bag (auto-wired by register)
user / current_user / guest / check Auth stubs reading context (filled by nyxa-auth)
env / config / settings Process env + BaseSettings under config/ (model_dump + typed settings(stem))
Route / Controller Routing DSL + controller base (constructor Depends OK)
route(name, **params) Reverse URL helper from compiled named routes
FormRequest Pydantic request base (authorize() → ForbiddenError when falsy)
ServiceProvider App boot hooks (register / boot) discovered under providers/
bind / singleton / instance / make / resolve / inject / has Lean service container (flushed on register)
ForbiddenError HTTP 403 AppError subclass
register(app) Default AppError handler, custom handlers, middleware, context, container flush, providers, routes.py, listeners
dispatcher / EventDispatcher In-process events

Everything else under nyxa.* (templates, generate, make, config internals, …) is private and may change without notice.

See also API.md.


Config (nyxa.toml, optional)

All keys optional. Defaults:

app = "myapp"       # required in practice — set by `init` / `new`
src = "src"

[dev]
module = "myapp.main:app"   # default: "{app}.main:app"
host = "0.0.0.0"
port = 8000
reload = true

[paths]
errors = "http/exception/errors"
handlers = "http/exception/handlers"
requests = "http/requests"
responses = "http/responses"
middleware = "http/middleware"
controllers = "http/controllers"
dependencies = "http/dependencies"
services = "services"
schemas = "schemas"
enums = "domain/enums"
config = "config"
events = "events"
listeners = "listeners"
jobs = "jobs"
providers = "providers"
tests = "tests"             # relative to project root

[middleware]
# global = ["timing"]       # optional Kernel-style list (module stems); request-inbound order
# When unset, all http/middleware/*/Middleware exports are auto-discovered (alphabetical).

[providers]
# boot = ["app_service_provider"]  # optional ordered list (module stems under paths.providers)
# When unset, all providers/*/Provider exports are auto-discovered (alphabetical).

[env]
file = ".env"               # primary dotenv under project root
# environment = "local"     # also load .env.{environment}; else NYXA_ENV / APP_ENV

[bases]
error = "nyxa.errors:AppError"

Fallback: [tool.nyxa] app = "..." in pyproject.toml.

Middleware (two concepts)

Mechanism Role
Route.middleware(...) / Route.group(..., middleware=[...]) FastAPI Depends on routes (per-route / group)
Discovered Middleware export under http/middleware/ Global Starlette BaseHTTPMiddleware stack via register

Use [middleware] global = ["timing", ...] for an explicit Kernel-style order (first entry is outermost / sees the request first). Omit global to auto-discover all modules alphabetically.

Service providers

App boot hooks under providers/. Each module exports Provider (a ServiceProvider subclass) with optional register / boot methods. register(app) calls all register hooks, then all boot hooks, before loading routes.py.

uv run nyxa make:provider AppService
[providers]
boot = ["app_service_provider"]  # optional; omit for alphabetical discovery

Service container

Lean bind/resolve for app-scoped services. Bind in a provider; inject via FastAPI:

from nyxa import singleton, inject

# in ServiceProvider.register:
singleton(UserService)

# in controller __init__:
service: Annotated[UserService, Depends(inject(UserService))]

Also: bind, instance, make / resolve, has. The global container is flushed at the start of each register().

Environment (.env)

register(app) loads dotenv files into the process environment:

  1. Primary file from [env] file (default .env)
  2. Optional .env.{environment} when [env] environment is set, else NYXA_ENV, else APP_ENV

Among files, the environment-specific file overrides the primary for shared keys. Existing OS/CI variables are never overridden. Missing files are no-ops. init / new write .env.example as a template — copy to .env (and optionally .env.local, etc.) locally (gitignored).

[env]
file = ".env"
environment = "local"   # loads .env then .env.local

Out of scope (for now)

  • Database / migrations — Phase 2 (nyxa-db) — Done
  • Auth / policies — Phase 3 (nyxa-auth) — Done (JWT, sessions, personal tokens, Google OAuth)
  • Queues, cache, mail, storage — Phase 4 Done (nyxa-queue, nyxa-cache, nyxa-mail, nyxa-storage)
  • Frontend scaffolding — never (API-only)

Tests

# from workspace root
uv sync --group dev
uv run pytest packages/nyxa/tests

Publish (maintainers)

.github/workflows/publish-nyxa.yml publishes core nyxa and all seven nyxa-* packs from one Release tag, using PyPI Trusted Publishing (OIDC, no API token secret).

One-time setup

  1. In GitHub → Settings → Environments, create one environment per package: pypi-nyxa, pypi-nyxa-db, pypi-nyxa-auth, pypi-nyxa-queue, pypi-nyxa-cache, pypi-nyxa-mail, pypi-nyxa-storage, pypi-nyxa-postgres.
  2. On PyPI, add a Trusted Publisher for each package (GitHub tab):
PyPI project name Owner Repository name Workflow name Environment name
nyxa NyxaDev Nyxa publish-nyxa.yml pypi-nyxa
nyxa-db NyxaDev Nyxa publish-nyxa.yml pypi-nyxa-db
nyxa-auth NyxaDev Nyxa publish-nyxa.yml pypi-nyxa-auth
nyxa-queue NyxaDev Nyxa publish-nyxa.yml pypi-nyxa-queue
nyxa-cache NyxaDev Nyxa publish-nyxa.yml pypi-nyxa-cache
nyxa-mail NyxaDev Nyxa publish-nyxa.yml pypi-nyxa-mail
nyxa-storage NyxaDev Nyxa publish-nyxa.yml pypi-nyxa-storage
nyxa-postgres NyxaDev Nyxa publish-nyxa.yml pypi-nyxa-postgres

For a project that doesn't exist yet, register it as a pending publisher at pypi.org/manage/account/publishing. PyPI allows 3 pending publishers per account at a time, and each slot frees up once its project is created. Pending publishers must also be unique on owner/repo/workflow/environment, which is why every package has its own environment.

Each release

gh release create v0.1.1 --target master --title "Nyxa v0.1.1" --notes "…"
gh run watch

The tag (leading v stripped) becomes the version of every packages/nyxa*/pyproject.toml. The workflow builds all eight packages once, then runs one publish job per package in its own environment. A failed job can be re-run with gh run rerun <run-id> --failed; files already on PyPI are skipped.

Verify a build locally

From the workspace root, build exactly as CI does, then install the wheels into a scratch environment:

for p in nyxa nyxa-db nyxa-auth nyxa-queue nyxa-cache nyxa-mail nyxa-storage nyxa-postgres; do
  uv build --package "$p" --no-sources --out-dir /tmp/nyxa-dist
done
uvx twine check /tmp/nyxa-dist/*
uvx --from /tmp/nyxa-dist/nyxa-0.1.0-py3-none-any.whl nyxa new /tmp/demoapp --no-input
# resolve the scaffold against local wheels before they are on PyPI:
cd /tmp/demoapp && uv sync --find-links /tmp/nyxa-dist

Roadmap

Workspace plans: roadmap/README.md.

Release files for nyxa 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for nyxa 0.1.0
File Size Uploaded
nyxa-0.1.0.tar.gz 43.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for nyxa 0.1.0
File Interpreter ABI Platform
nyxa-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 95.8 kB

Release files / nyxa-0.1.0.tar.gz

Download URL nyxa-0.1.0.tar.gz
Size 43.1 kB
Tags Source
SHA-256 checksum
How to use checksums
08faef52d14564f9cb1adac73f8299d782effcfd2ea241f06d7caea9aee7d2d0
BLAKE2b-256 checksum
How to use checksums
aa6a68430f82e5ad89b576abbe163192a605d083c658cb9549f3fc7cfe57701a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.20 {"installer":{"name":"uv","version":"0.12.20","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}

Release files / nyxa-0.1.0-py3-none-any.whl

Download URL nyxa-0.1.0-py3-none-any.whl
Size 52.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
82fb3fc12a1bf2e82dd9d2da1c611d50dd6397d61ed6b55d181b069c2c9b4868
BLAKE2b-256 checksum
How to use checksums
a166cae43aa36b3bdcd9f67525ba87d36bdae8422e283715eab0b9856e9ecb6a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.20 {"installer":{"name":"uv","version":"0.12.20","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}

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page