Part 3: Controllers & Routes¶
Generate a controller for each model:
This created app/Controllers/project_controller.py and
app/Controllers/task_controller.py, each a Controller subclass with a
placeholder index method. Replace app/Controllers/project_controller.py:
from starlette.responses import JSONResponse, Response
from zeython import Controller, NotFoundException
from app.Models.project import Project
class ProjectController(Controller):
async def index(self, request):
projects = await Project.all()
return JSONResponse([project.to_dict() for project in projects])
async def show(self, request):
project = await Project.find(int(request.path_params["id"]))
if project is None:
raise NotFoundException("Project not found")
return JSONResponse(project.to_dict())
async def store(self, request):
data = await request.json()
project = await Project.create(name=data.get("name"))
return JSONResponse(project.to_dict(), status_code=201)
async def update(self, request):
project = await Project.find(int(request.path_params["id"]))
if project is None:
raise NotFoundException("Project not found")
data = await request.json()
await project.update(**data)
return JSONResponse(project.to_dict())
async def destroy(self, request):
project = await Project.find(int(request.path_params["id"]))
if project is None:
raise NotFoundException("Project not found")
await project.delete()
return Response(status_code=204)
Nothing here is Zeython-specific magic — it's plain async Python calling
the Active Record API from Part 2. NotFoundException
(and its siblings — ValidationException, UnauthorizedException,
ForbiddenException) are the framework's way of turning "this went
wrong" into the right HTTP status and JSON shape without you writing that
translation by hand every time; raise one, the framework handles the
response.
Do the same for app/Controllers/task_controller.py, swapping Project
for Task and name for title (and pass done through on create if
you want to set it explicitly — it defaults to False from the model
either way):
from starlette.responses import JSONResponse, Response
from zeython import Controller, NotFoundException
from app.Models.task import Task
class TaskController(Controller):
async def index(self, request):
tasks = await Task.all()
return JSONResponse([task.to_dict() for task in tasks])
async def show(self, request):
task = await Task.find(int(request.path_params["id"]))
if task is None:
raise NotFoundException("Task not found")
return JSONResponse(task.to_dict())
async def store(self, request):
data = await request.json()
task = await Task.create(title=data.get("title"))
return JSONResponse(task.to_dict(), status_code=201)
async def update(self, request):
task = await Task.find(int(request.path_params["id"]))
if task is None:
raise NotFoundException("Task not found")
data = await request.json()
await task.update(**data)
return JSONResponse(task.to_dict())
async def destroy(self, request):
task = await Task.find(int(request.path_params["id"]))
if task is None:
raise NotFoundException("Task not found")
await task.delete()
return Response(status_code=204)
Wire up the routes¶
Open routes/web.py, import both controllers, and register a full REST
resource for each:
from app.Controllers.project_controller import ProjectController
from app.Controllers.task_controller import TaskController
app.router.resource("/projects", ProjectController)
app.router.resource("/tasks", TaskController)
resource() maps one controller onto the standard five CRUD routes in
one line:
| Method | Path | Controller method |
|---|---|---|
GET |
/projects |
index |
POST |
/projects |
store |
GET |
/projects/{id} |
show |
PUT/PATCH |
/projects/{id} |
update |
DELETE |
/projects/{id} |
destroy |
Pass only=("index", "show") if you only want a subset — the generated
UserController/PostController in routes/web.py already do this for
routes that shouldn't exist yet (see the finished file).
Try it¶
zeython serve picked up the changes automatically. Create a project,
then a task:
curl -sS -X POST http://127.0.0.1:8000/projects \
-H 'Content-Type: application/json' \
-d '{"name": "Website Redesign"}'
{"name":"Website Redesign","id":1,"created_at":"...","updated_at":"...","is_deleted":false,"deleted_at":null}
curl -sS -X POST http://127.0.0.1:8000/tasks \
-H 'Content-Type: application/json' \
-d '{"title": "Design the new homepage"}'
curl -sS http://127.0.0.1:8000/tasks
curl -sS http://127.0.0.1:8000/tasks/1
curl -sS -X PUT http://127.0.0.1:8000/tasks/1 \
-H 'Content-Type: application/json' \
-d '{"done": true}'
curl -sS -X DELETE http://127.0.0.1:8000/tasks/1 -o /dev/null -w '%{http_code}\n'
The last command prints 204 — a successful delete with no body.
You now have full CRUD for two models. What's missing: a task has no idea which project it belongs to. Continue to Part 4 — Relationships.