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:
- Primary file from
[env] file(default.env) - Optional
.env.{environment}when[env] environmentis set, elseNYXA_ENV, elseAPP_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 Release (recommended)
.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
- 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. - 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)
| File | Size | Uploaded | |
|---|---|---|---|
| nyxa-0.1.0.tar.gz | 43.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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}
|