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
NotFoundResponseDirectiveso the API layer renders 404. The default200is 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).