How get relates to get_obj, post to create_obj, …
Every HTTP verb in BaseResource is implemented as a thin "handler" that orchestrates a "piece" — the function that does the actual work. Knowing which one to override saves you from accidentally rewriting cache invalidation, response wrapping or ownership checks.
Two layers, one verb
Each HTTP method has two methods on the resource:
| HTTP verb | Handler (orchestrates) | CRUD piece (does the work) |
|---|---|---|
| GET | get(request) | get_obj(id) for detail, get_objs(request) for list |
| POST | post(request) | create_obj(request, body) |
| PATCH | patch(request) | update_obj(id, body) |
| DELETE | delete(request) | delete_obj(id) |
What the handler does (and what the piece does not)
The handler:
- Decides whether the request is list vs detail (GET only — based on the URL
id) - Calls the right piece
- Calls
alter_list/alter_detailon the result - Applies
normalize_list/normalize_objif set - Calls
serialize(which runsdehydrate,post_process,save_cache, encodes JSON) - Calls
invalidate_cacheon writes (POST/PATCH/DELETE)
The piece:
- Talks to the database
- Validates the body against
create_fields/update_fields - Auto-fills
created_by,updated_by,owner_idfromself.user - Saves m2m and
custom_*fields - Applies
_ownership_filter()(DELETE)
GET — the special one
GET is the only handler that can route to two different pieces depending on the URL:
async def get(self, request):
if self.id:
data = await self.get_obj(self.id) # detail
data = await self.alter_detail(data)
...
else:
data = await self._get_objs(request) # list
...Internally _get_objs calls get_objs then return_results (which wraps with meta + paginates). When you override get_objs you keep the wrap; when you override get you replace everything.
Pick the lowest layer that does the job
| I want to… | Override |
|---|---|
| Strip a sensitive column from every row | dehydrate(row) |
| Lower-case incoming emails | hydrate(body) |
| Add a computed column to the list response | get_objs (call super()) |
| Send a welcome email after signup | create_obj (call super() first, then mail) |
| Forbid an update unless user is owner | update_obj (raise HTTPException before super()) |
| Soft-delete instead of real DELETE | delete_obj (don't call super() — replace it) |
| Return raw bytes instead of JSON | get / post / etc. (replace the handler) |
Examples
Add a computed field — get_objs + super()
async def get_objs(self, request):
rows = await super().get_objs(request)
for row in rows:
row['display_name'] = row.get('name', '').title()
return rowssuper() runs the standard query (filters, pagination, ordering, related fields, prefetch). You add the computed column on top.
Side effect after create — create_obj + super()
async def create_obj(self, request, body):
result = await super().create_obj(request, body)
await send_welcome_email(self.obj)
return resultself.obj is set by super().create_obj after the row is created — so the side effect runs after persistence.
Soft-delete — delete_obj without super()
async def delete_obj(self, id):
obj = await self.queryset.aget(pk=id)
obj.deleted = True
await obj.asave()
return {'success': True, 'id': id, 'message': 'Soft-deleted'}Here we don't call super() because we don't want the row removed. The handler still calls invalidate_cache afterwards — your soft-delete still drops the cache for free.
Replace the GET response entirely — get
async def get(self, request):
if self.id:
return HttpResponse(b'<binary>', content_type='application/octet-stream')
return await super().get(request)When the response is not JSON-shaped, replacing the whole get is cleaner than fighting serialize.
Rule of thumb: if you can do it from super(), do. The plumbing around the piece — cache invalidation, ownership checks, response wrapping — is what you actually want, not what you want to rewrite.
0-mcp by Stamatios Stamou Jr — github.com/ssjunior/0-mcp