# 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 ```python # 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 ```python # 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 {doc}`/manager`). * Failures attach a `NotFoundResponseDirective` so the API layer renders **404**. The default `200` is replaced by the directive (see {ref}`responses-directives-and-http-status-codes`). --- ## Entry point ```python # 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 ```bash # 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 {doc}`/api_content_negotiation`).