Add product search to the catalog API
Python · Django · beginner · modification
Adds the search box endpoint for the storefront. It trims and validates `q`, then runs one small SQL query: active products whose lowercased name contains the lowercased search text, sorted by name and capped at 50. Kept it as a single raw query so it stays obvious what hits the database. Tried it with a few product names, mixed case and an empty query.
Storefront for a small online shop. PostgreSQL 16. The `catalog` app already has `Product` (`name` CharField max 200, `description` TextField, `price` DecimalField, `is_active` BooleanField; table `catalog_product`), `ProductListSerializer` (fields `id`, `name`, `price`) and the list endpoint; `config/urls.py` already includes `catalog.urls` under `api/`. The same database holds customer accounts (Django's built-in `User` model, table `auth_user`) and orders.
Requirements
- Add `GET /api/products/search/?q=<text>` to the existing `catalog` app. It returns the active products whose name contains `q` as literal text, case-insensitively as PostgreSQL's `lower()` compares them, ordered by name, at most 50 of them.
- `q` is trimmed of surrounding whitespace. Missing or empty after trimming: 400 with `{"detail": "q is required"}`. Longer than 100 characters, or containing a NUL character: 400.
- The endpoint is public (no sign-in) and read-only, like the product list.
- The response body is a JSON array of results, each serialized with the existing `ProductListSerializer` (`id`, `name`, `price`).
Files touched
- catalog/views.py
- catalog/urls.py
--- catalog/views.py
from rest_framework import generics
from rest_framework.permissions import AllowAny
+from rest_framework.response import Response
+from rest_framework.views import APIView
from .models import Product
from .serializers import ProductListSerializer
+
+MAX_QUERY_LENGTH = 100
permission_classes = [AllowAny]
+
+class ProductSearchView(APIView):
+ permission_classes = [AllowAny]
+
+ def get(self, request):
+ q = request.query_params.get("q", "").strip()
+ if not q:
+ return Response({"detail": "q is required"}, status=400)
+ if len(q) > MAX_QUERY_LENGTH:
+ return Response({"detail": f"q must be at most {MAX_QUERY_LENGTH} characters"}, status=400)
+ if "\x00" in q:
+ return Response({"detail": "q must not contain NUL characters"}, status=400)
+ products = Product.objects.raw(
+ "SELECT id, name, price FROM catalog_product "
+ f"WHERE is_active AND strpos(lower(name), lower('{q}')) > 0 "
+ "ORDER BY name LIMIT 50"