REST CRUD with validation

A self-contained example showing the four standard REST verbs on a single resource (/items / /items/{item_id}), with input validation through inject(...) and a manager that owns an in-memory store.

The example uses only built-in EDRI primitives — no database — so it stays runnable as written.


Events and responses

# events/items.py
from http import HTTPMethod

from edri.api.dataclass.api_event import api, ApiType
from edri.dataclass.event import Event, event
from edri.dataclass.response import Response, response
from edri.utility.function import inject
from edri.utility.validation import IntegerValidator, StringValidator


# --- shared response shapes ---

@response
class ItemResponse(Response):
    id: int
    name: str
    quantity: int


@response
class ItemListResponse(Response):
    items: list[dict]


@response
class EmptyResponse(Response):
    pass


# --- list ---

@api(url="/items", method=HTTPMethod.GET, exclude={ApiType.HTML, ApiType.WS})
class ItemList(Event):
    response: ItemListResponse


# --- read one ---

@api(url="/items/{item_id}", method=HTTPMethod.GET, exclude={ApiType.HTML, ApiType.WS})
class ItemGet(Event):
    item_id: inject(IntegerValidator, minimum=1)
    response: ItemResponse


# --- create ---

@api(url="/items", method=HTTPMethod.POST, exclude={ApiType.HTML, ApiType.WS})
class ItemCreate(Event):
    name: inject(StringValidator, minimum_length=1, maximum_length=100)
    quantity: inject(IntegerValidator, minimum=0)
    response: ItemResponse


# --- update ---

@api(url="/items/{item_id}", method=HTTPMethod.PUT, exclude={ApiType.HTML, ApiType.WS})
class ItemUpdate(Event):
    item_id: inject(IntegerValidator, minimum=1)
    name: inject(StringValidator, minimum_length=1, maximum_length=100)
    quantity: inject(IntegerValidator, minimum=0)
    response: ItemResponse


# --- delete ---

@api(url="/items/{item_id}", method=HTTPMethod.DELETE, exclude={ApiType.HTML, ApiType.WS})
class ItemDelete(Event):
    item_id: inject(IntegerValidator, minimum=1)
    response: EmptyResponse

Notes:

  • inject(IntegerValidator, minimum=1) turns the path segment into a positive integer and rejects the request with 400 if validation fails.

  • exclude={ApiType.HTML, ApiType.WS} keeps the endpoint REST-only.


Manager

# managers/items.py
from itertools import count

from edri.abstract import ManagerBase, request
from edri.dataclass.response import ResponseStatus
from edri.dataclass.directive.http import NotFoundResponseDirective

from events.items import (
    ItemCreate, ItemDelete, ItemGet, ItemList, ItemUpdate,
)


class ItemsManager(ManagerBase):
    """Owns a small in-memory catalogue."""

    def __init__(self, router_queue):
        super().__init__(router_queue)
        self._items: dict[int, dict] = {}
        self._ids = count(start=1)

    @request
    def list_items(self, event: ItemList) -> None:
        event.response.items = list(self._items.values())

    @request
    def get_item(self, event: ItemGet) -> None:
        item = self._items.get(event.item_id)
        if item is None:
            event.response.set_status(ResponseStatus.FAILED)
            event.response.add_directive(NotFoundResponseDirective())
            return
        event.response.id = item["id"]
        event.response.name = item["name"]
        event.response.quantity = item["quantity"]

    @request
    def create_item(self, event: ItemCreate) -> None:
        new_id = next(self._ids)
        record = {"id": new_id, "name": event.name, "quantity": event.quantity}
        self._items[new_id] = record
        event.response.id = record["id"]
        event.response.name = record["name"]
        event.response.quantity = record["quantity"]

    @request
    def update_item(self, event: ItemUpdate) -> None:
        item = self._items.get(event.item_id)
        if item is None:
            event.response.set_status(ResponseStatus.FAILED)
            event.response.add_directive(NotFoundResponseDirective())
            return
        item["name"] = event.name
        item["quantity"] = event.quantity
        event.response.id = item["id"]
        event.response.name = item["name"]
        event.response.quantity = item["quantity"]

    @request
    def delete_item(self, event: ItemDelete) -> None:
        if event.item_id not in self._items:
            event.response.set_status(ResponseStatus.FAILED)
            event.response.add_directive(NotFoundResponseDirective())
            return
        del self._items[event.item_id]
        event.response.set_status(ResponseStatus.OK)

Things worth noting:

  • The handler does not call event_send(...) — responses on a manager are sent automatically when the handler returns (see Manager).

  • Failures attach a NotFoundResponseDirective so the API layer renders 404. The default 200 is replaced by the directive (see Responses, directives, and HTTP status codes).


Entry point

# main.py
from edri import EDRI

from managers.items import ItemsManager

if __name__ == "__main__":
    edri = EDRI()
    edri.add_component(ItemsManager(edri.router_queue))
    edri.start_api()
    edri.run()

Calling it

# create
curl -X POST http://localhost:8080/items \
     -H "Accept: application/json" \
     -H "Content-Type: application/json" \
     -d '{"name": "widget", "quantity": 5}'

# list
curl -H "Accept: application/json" http://localhost:8080/items

# get
curl -H "Accept: application/json" http://localhost:8080/items/1

# update
curl -X PUT http://localhost:8080/items/1 \
     -H "Accept: application/json" \
     -H "Content-Type: application/json" \
     -d '{"name": "widget", "quantity": 12}'

# delete
curl -X DELETE -H "Accept: application/json" http://localhost:8080/items/1

Remember that REST clients must send Accept: application/json to get JSON back (see Content Negotiation).