From ac337e3401365297604bc741e08cfee51f9e7143 Mon Sep 17 00:00:00 2001 From: Rafael Bradley Date: Thu, 16 Jul 2026 02:32:06 -0300 Subject: [PATCH] Update README --- README.md | 483 +++++------------------------------------------------- 1 file changed, 43 insertions(+), 440 deletions(-) diff --git a/README.md b/README.md index 93e4ce0..9f67a3b 100644 --- a/README.md +++ b/README.md @@ -1,451 +1,54 @@ -# Uv + FastAPI + Tortoise | Template +# Nyeki's API -A template FastAPI project with Tortoise ORM integration. +This is a simple API I built for a few utilities I've found myself recoding over and over or using +across projects. The documentation can be found at https://api.nyeki.dev/ and at the root endpoint +if you're self-hosting. -## Project Structure +This API provides no stability guarantees (unless you're self-hosting it and modify it). It's not +made for mass usage and with the small set of features it has, if you need something +production-ready you'll be better off writing something yourself. -This project is divided into 3 sub-modules, `project.api`, `project.db`, and `project.lib`. +## Self-hosting -- `project.api`: All the API-related code and the public-facing part of your code. -- `project.db`: DB models, fields, migration, setup, and more. The storage part of - your code. -- `project.lib`: All the business logic of your code. External API calls, internal - utilities, and any other internal code goes here. +This API is quite simple to self-host. -### API +Env variables: -The `project.api` module contains all the `FastAPI` code of your project. Routers and -schemas all go in here. The module is divided into `v*` sub-modules to version the API -by default. +- `NYEKI_DEBUG`: Whether to start in debug mode. +- `NYEKI_GITHUB_TOKEN`: A PAT token. You can get one + [here](https://github.com/settings/tokens). +- `NYEKI_DISCORD_TOKEN`: A bot token. You can get one + [here](https://discord.com/developers/applications/). +- `NYEKI_BIND_HOST`: Docker-only. The IP to bind the API to. Defaults to `0.0.0.0`. +- `NYEKI_BIND_PORT`: Docker-only. The port to bind the API to. Defaults to `8000`. +- `NYEKI_WORKERS`: Docker-only. The amount of async web workers to spawn. Defaults to + `$(nproc)` (the amount of CPU cores). +- `NYEKI_REDIS_URL`: The Redis URL to use for caching. -#### Handler Groups - -Each API version module is divided in sub-groups. Each of them usually represents a -single OpenAPI tag, and a group of related handlers. For example, to operate on a -`Book` resource you may have a `project.api.v1.books` module with its respective -sub-modules. - -Sub-modules inside those tag modules usually include: - -- `router.py`: The API router and all the API view handler code. -- `schemas.py`: All the handler-specific API schemas. - -For example, a minimal `router.py` file could look like this: - -```py -from fastapi import APIRouter - -from project.api.v1.example.schemas import MessageSchema - - -router = APIRouter(tags=["Example"]) - - -@router.get("/hello") -async def hello_world() -> MessageSchema: - """An example API endpoint that returns a hello world message.""" - - return MessageSchema(message="Hello, World!") -``` - -That example was taken from the example API module bundled by default with this -template. Check it out at `project/api/v1/example/router.py`. - -The code snippet above references `project.api.v1.example.schemas`, the `schemas.py` -file mentioned above. The example file looks like this: - -```py -from pydantic import BaseModel - - -class MessageSchema(BaseModel): - message: str -``` - -For more information about FastAPI return types and Pydantic models, check [FastAPI's -tutorial on response types](https://fastapi.tiangolo.com/tutorial/response-model/). - -To create new handler groups, create a new directory module under `project/api/v1` and -name it to your group. Inside it, create an empty `__init__.py` file and a `router.py` -file with the following base code: - -```py -from fastapi import APIRouter - - -router = APIRouter() -``` - -Next, go to `project/api/v1/__init__.py` and import the router aliasing it to -`{module}_router`, e.g. `books_router`, `auth_router`, etc. - -Last but not least, at the bottom of the file, add the following line: - -```py -app.include_router(module_router) -``` - -You can check the example module's importing and inclusion lines for a practical -example. - -#### API-Wide Schemas - -Sometimes, you have version-wide schemas. That may be the response base schema, -pagination schemas, error schemas, or any other schemas the whole API uses. In those -cases, you can use the `project/api/v*/schemas.py` module and include them there. - -For example, given that you want a base response schema where data is always in a -`data` property in an object (JSON), your `project/api/v1/schemas.py` would look like -this: - -```py -from pydantic import BaseModel - - -class ResponseSchema[T](BaseModel): - data: T -``` - -The included `project/api/v1/schemas.py` file includes a `NanoID` type by default. It -represents NanoID fields, as the name conveys, and it's useful when using nanoids for -your models instead of numeric IDs, UUIDs, or any other ID type. You can use it like -follows: - -```py -from pydantic import BaseModel, Field - -from project.api.v1.schemas import NanoID - - -class ExampleSchema(BaseModel): - id: NanoID - # Or - id: NanoID = Field(..., description="This example's ID.") -``` - -It can also be used as path parameters, query parameters, and anywhere that takes a -pydantic model in FastAPI. For example: - -```py -from project.api.v1.schemas import NanoID - - -@router.get("/example/{example_id}") -def get_example(example_id: NanoID) -> None: - ... -``` - -#### Schema Conventions - -Schemas are always named `*Schema` in this template to differenciate from models. - -For example, a `Book` model cannot have a `Book` schema because it'd cause name -conflicts in `router.py` files. A `BookSchema` schema allows you to differenciate the -schema from the model easily. - -Additionally, model-representing schemas have a `from_orm(cls, obj: Model)` method that -simplifies the conversion of models to schemas. For example, taking our example `Book` -model in `project/db/models/example.py`, a `BookSchema` would look like follows: - -```py -from datetime import datetime - -from pydantic import BaseModel - -from project.db import Book -from project.api.schemas import NanoID - - -class BookSchema(BaseModel): - id: NanoID - - title: str - author: str - - published_at: datetime - - created_at: datetime - updated_at: datetime - - @classmethod - def from_orm(cls, obj: Book) -> "BookSchema": - return cls( - id=obj.id, - title=obj.title, - author=obj.author, - published_at=obj.published_at, - created_at=obj.created_at, - updated_at=obj.updated_at, - ) -``` - -Your handler code would then look like: - -```py -from project.db import Book -from project.api.v1.errors import NotFoundError -from project.api.v1.schemas import ErrorSchema, NanoID -from project.api.v1.books.schemas import BookSchema - - -@router.get("/books/{book_id}", responses={ - 200: BookSchema, - 404: ErrorSchema, - 422: ErrorSchema, - 500: ErrorSchema, -}) -async def get_book_by_id(book_id: NanoID) -> BookSchema: - """Fetches a book by ID.""" - - book = Book.get_or_none(id=book_id) - - if book is None: - raise NotFoundError("book") - - return BookSchema.from_orm(book) -``` - -#### Errors - -You usually want to have a standard error schema within your API for your clients to -easily parse errors. This template makes it easy to raise errors in a standard way. - -The `project/api/errors.py` file contains a few pre-defined error types you can raise -right away from your API handlers to return errors. For example: - -```py -from project.api.v1.errors import NotFoundError -from project.api.v1.schemas import ErrorSchema - - -@router.get("/not-found", status_code=404) -def not_found() -> ErrorSchema: - raise NotFoundError("duck") -``` - -When calling that endpoint, the error will look like: - -```json -{ - "title": "Not Found", - "message": "The requested duck was not found" -} -``` - -The response status code will be `404 Not Found`. - -Any error subclassing `project.api.errors.BaseError` will be handled and returned -following the schema you saw above. You can follow the default error types provided to -create your own, customize messages, customize the response schema, and more. - -For example, to create a `418 I'm a Teapot` raisable error type, you'd do: - -```py -class ImATeapotError(ErrorInitMixin, BaseError): - title = "I'm a Teapot" - message = "Coffee? That's for losers, we drink Toy Story-themed tea here." - status_code = 418 -``` - -Your handler will then look something like: - -```py -from project.api.v1.errors import ImATeapotError -from project.api.v1.schemas import ErrorSchema - - -@router.get("/coffee", status_code=418) -def get_coffee() -> ErrorSchema: - raise ImATeapotError() -``` - -To customize the error schema, update the `ErrorSchema` class definition in -`project/api/schemas.py` and update the `project.api.errors.BaseError.handler` method -to reflect those changes. - -#### Documentation - -The documentation you're reading here has built-in support for writing it using -markdown files. - -Documentation files live under the `docs/` folder, next to the `project/` folder at the -top level of the repository. Each file is named after the OpenAPI tag it documents, -like the bundled-in `Example.md` file. Any markdown files you create in there (prefixed -with `.md`) will create a tag named after the file's name (without the extension) in -your OpenAPI docs documented with the file's contents. - -The only special name there is is the `_Project.md` file, which documents the API at -the root level. - -### Database - -This template uses [Tortoise ORM](https://tortoise.github.io/), a simple and ergonomic -ORM that's Django ORM-like but prettier. - -This guide does not focus on teaching you how to use Tortoise ORM, rather on the -project structure of this template. Check their documentation for more information -about ORM usage. - -The `project.db` module has a few sub-modules whose purpose can be inferred based on -the naming. The modules you'll most commonly edit are the following: - -- `project.db` (`__init__.py`): This file re-exports models to make importing - elsewhere easier. -- `project.db.models`: Contains concern-specific submodules with database model - definitions. For example, `users.py` for `User` models, `books.py` for `Book` and - `Author` models (e.g. in a books-related application). -- `project.db.migrations`: Contains migrations, autogenerated by the `tortoise` CLI. -- `project.db.fields`: Custom DB fields and field aliases. It contains a - `NanoIDField` function which aliases to a nanoid `CharField`. Add any custom DB - fields here. -- `project.db.lifespan`: Contains `on_startup()` and `on_shutdown()`. They get called - from `project.api.lifespan` when the API goes up and down, applying migrations and - initializing DB connections on startup and closing connections on shutdown. - -#### Model Modules - -Models are divided into modules. For example, you may create a `users.py` module for -user-related models like `User` and `Session`, and a `books.py` for `Book` and `Author` -models following the books app example. - -These modules contain nothing but model definitions. For example, the bundled-in -`example.py` contains: - -```py -from tortoise import fields -from tortoise.models import Model - -from project.db.fields import NanoIDField - - -class Book(Model): - id = NanoIDField(primary_key=True) - - title = fields.CharField(max_length=255) - author = fields.CharField(max_length=255) - - published_at = fields.DatetimeField(null=True) - - created_at = fields.DatetimeField(auto_now_add=True) - updated_at = fields.DatetimeField(auto_now=True) -``` - -These modules are pointed at in `project.lib.settings.DATABASE`, which looks like this -by default: - -```py -CONFIG = { - "connections": {"default": os.getenv("DATABASE_URL", "sqlite://db.sqlite3")}, - "apps": { - "models": { - "models": [ - "project.db.models.example", - ], - "default_connection": "default", - "migrations": "project.db.migrations", - } - }, - "use_tz": True, - "timezone": "UTC", -} -``` - -To create a new models module, create a new module under `project/db/models` and add it -to the `apps.models.models` list in the `project.lib.settings.DATABASE` dict. - -Last but not least, re-export your models from `project/db/__init__.py`. It's a little -QoL thing that makes your life easier later on when importing models. For example, the -default `project/db/__init__.py` looks like this: - -```py -from project.db.models.example import Book as Book -``` - -To add a new model, just add it to the imports list. - -#### Migrations - -This template uses the Tortoise ORM CLI to handle migrations. Migrations are -automatically-generated and stored under `project/db/migrations`. - -To create a new migration after you make an update, use `tortoise migrate`. - -The server automatically applies any pending migrations on startup. If you wish to -apply such new migrations manually, run `tortoise upgrade`. - -### Lib and Business Logic - -All your internal business logic goes in the `project/lib/` directory. - -For example, if you need to add a cache backend to your server, you could create a -`cache.py` file under `project/lib/` containing your caching code, or a directory, you -choose. - -#### Project Settings - -The template comes with a `settings.py` file which works just like a Django -`settings.py` file. In case you're not familiar with Django, it's just a file with -setting constants. `DATABASE_URL`, `REDIS_URL`, `THIRD_PARTY_SERVICE_API_KEY`, and any -other constants go there. - -This template comes with support for `.env` files by default. - -It's a good practice to prefix your project's environment variables with `PROJECT_` or -your project's name. It prevents conflicts when running multiple services in the same -environment. - -## Getting Started - -To start with, delete the example models and API router (you can keep it if you want a -base to work on). - -To do that: - -1. Delete the `project/api/v1/example` directory. -2. Delete the `docs/v1/Example.md` file. -3. Open `project/api/v1/__init__.py` and remove the inclusion of the example handler. -4. Delete the `project/db/models/example.py` file. -5. Open `project/db/settings.py` and remove `project.db.models.example` from the list - of models. -6. Open `project/db/__init__.py` and remove the re-export of the `Book` example model. - -### Template Defaults Cleanup - -To get started with, you may want to upgrade dependencies and rename the project to -something you like better. - -To do that, do the following: - -1. Replace all case-sensitive appearances of `project` under the `project/` directory - with your new import name. -2. Replace all case-sensitive appearances of `Project` under the `project/` directory - with your project's name. -3. Rename the `project/` directory to your new import name. -4. Rename your project in `pyproject.toml`. -5. Update the `[tool.tortoise]` section in your `pyproject.toml` file to point to the - new directory and root module name. - -The following steps mention deletion, but you can always just update those files -instead if you want to keep them as a base for starting. It also still mentions -`project/`, which by this time you'll already have renamed. Assume `project/` means -your now-renamed source code folder. - -6. Empty the `docs/` directory. -7. Delete the `project/api/v1/example/` directory. -8. Remove `project.api.v1.example` imports and router inclusion from - `project/api/v1/__init__.py`. -9. Delete the `project/db/models/example.py` file. -10. Remove the `project.db.models.example.*` re-exports from `project/db/__init__.py`. -11. Remove `"project.db.models.example"` from the `DATABASE` object in - `project/lib/settings.py`. -12. Delete the `project/db/migrations/` directory. - -### Run the Server - -To start the server, run: +You can run it using uv by doing: ```sh -$ uv run uvicorn project:app --reload --reload-include "docs/**/*.md" --reload-include .env +$ uv run uvicorn nyekiapi:app ``` -`project` is your import name. +To run deploy the API with Docker, just run: + +```sh +$ docker run git.moan.dev/nyeki/nyeki-api:v2.0.0 -p 8000:80 \ +$ -e NYEKI_DISCORD_TOKEN="..." \ +$ -e NYEKI_GITHUB_TOKEN="..." +``` + +You can also clone this repository and run: + +```sh +$ docker build -t nyeki-api . +$ docker run nyeki-api -p 8000:80 \ +$ -e NYEKI_DISCORD_TOKEN="..." \ +$ -e NYEKI_GITHUB_TOKEN="..." +``` + +## Support + +Enjoyed using this? Consider supporting this and other projects like this by +[making a donation here](https://ko-fi.com/nyeki). It really helps me keep this up and running!