A fully production-ready e-commerce backend built with Django REST Framework, deployed on Railway, containerized with Docker, and engineered with real-world backend practices including async task processing, JWT authentication, Redis caching, performance profiling, and automated testing.
Built to demonstrate backend engineering depth β not just CRUD.
π Live API: sharar-prod.up.railway.app
π¦ GitHub: github.com/ays19/storefront
Most Django projects stop at basic CRUD. This one goes further:
- π JWT authentication with Djoser β register, login, token refresh, user profile
- π UUID-based cart system β carts can't be enumerated or guessed by clients
- βοΈ Atomic order transactions β cart-to-order conversion is fully atomic; no partial data
- ποΈ Django ORM optimization β
annotate(),select_related(),prefetch_related(),only(), andbulk_create()used throughout to minimize queries and maximize performance - π‘ Django Signals β order creation triggers decoupled downstream events
- π₯ Group-based permissions β scalable access control, not per-user assignments
- βοΈ Celery + Redis β async background tasks with scheduled jobs via Celery Beat
- π Silk profiling β every SQL query tracked; N+1 problems caught before production
- π Locust load testing β realistic traffic simulation with weighted task distribution
- π³ Docker Compose β full 7-service local environment in one command
- βοΈ Railway deployment β live production API with automated deploy pipeline
| Layer | Technology |
|---|---|
| Language | Python 3.12 |
| Framework | Django 6.0.4 + Django REST Framework |
| ORM | Django ORM (annotations, select_related, prefetch_related, bulk_create) |
| Database | MySQL 8.0 |
| Cache & Broker | Redis |
| Auth | JWT (SimpleJWT + Djoser) |
| Async Tasks | Celery + Celery Beat |
| Task Monitor | Flower |
| Profiling | Django Silk |
| Load Testing | Locust |
| Testing | Pytest + pytest-django |
| Email (dev) | smtp4dev |
| Containerization | Docker + Docker Compose |
| Deployment | Railway (Nixpacks) |
| Static Files | WhiteNoise |
storefront/
βββ store/ # Core business logic β products, orders, carts, customers
β βββ models.py # Domain models with UUID, signals, custom permissions
β βββ views.py # ViewSets with filtering, search, sorting, pagination
β βββ serializers.py # Nested serializers, computed fields, validation
β βββ permissions.py # Custom permission classes
β βββ filters.py # django-filter integration
β βββ signals/ # Order created signal
β βββ admin.py # Fully customized admin panel
β βββ tests.py # Pytest test suite
βββ core/ # Custom User model, user signals, Djoser serializers
βββ playground/ # Celery task demos
βββ tags/ # Generic relationships via ContentTypes
βββ likes/ # Generic like system
βββ locustfiles/ # Load testing scenarios
βββ storefront/
βββ settings/
β βββ common.py # Shared config β JWT, Celery, logging, email
β βββ dev.py # Local dev overrides
β βββ prod.py # Production config β env vars, CSRF, console logging
βββ celery.py # Celery app config
βββ wsgi.py
# Annotating querysets instead of extra queries
Collection.objects.annotate(products_count=Count('products'))
# Avoiding N+1 with select_related and prefetch_related
Cart.objects.prefetch_related('items__product').all()
Customer.objects.select_related('user').all()
# Bulk insert instead of N individual queries
OrderItem.objects.bulk_create(order_items)
# Fetch only needed fields
Customer.objects.only('id').get(user_id=user.id)class Cart(models.Model):
id = models.UUIDField(primary_key=True, default=uuid4)Integer IDs are sequential and guessable. UUIDs make cart enumeration impossible β a deliberate security choice.
def save(self, **kwargs):
with transaction.atomic():
order = Order.objects.create(customer=customer)
OrderItem.objects.bulk_create(order_items)
Cart.objects.filter(pk=cart_id).delete()
order_created.send_robust(self.__class__, order=order)Cart-to-order conversion is wrapped in a transaction. If anything fails, nothing is committed. bulk_create replaces N individual inserts with one query.
# core/signals/handlers.py
@receiver(order_created)
def on_order_created(sender, **kwargs):
print(kwargs['order']) # send email, trigger webhook, log β decoupled
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_customer_for_new_user(sender, **kwargs):
if kwargs['created']:
Customer.objects.create(user=kwargs['instance'])Business logic is decoupled from the signal sender. The order serializer doesn't need to know what happens after an order is created.
class IsAdminOrReadOnly(permissions.BasePermission):
def has_permission(self, request, view):
if request.method in permissions.SAFE_METHODS:
return True
return request.user and request.user.is_staff
class ViewCustomerHistoryPermission(permissions.BasePermission):
def has_permission(self, request, view):
return request.user.has_perm('store.view_history')Permissions are assigned to Groups, not individual users. Adding a user to Content Manager gives them all required permissions instantly.
def get_queryset(self):
if user.is_staff:
return Order.objects.all()
customer_id = Customer.objects.only('id').get(user_id=user.id)
return Order.objects.filter(customer_id=customer_id)Users can only see their own orders. Staff sees everything. Enforced at the queryset level β not in the view.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | store/products/ |
No | List products with filter/search/sort/pagination |
| GET | store/products/{id}/ |
No | Single product with images |
| GET | store/products/{id}/reviews/ |
No | Product reviews |
| POST | store/products/{id}/images/ |
Admin | Upload product image |
| GET | store/collections/ |
No | Collections with product count |
| POST | store/carts/ |
No | Create cart (returns UUID) |
| GET/POST | store/carts/{id}/items/ |
No | View/add cart items |
| PATCH | store/carts/{id}/items/{id}/ |
No | Update item quantity |
| GET/POST | store/orders/ |
β JWT | List own orders / place order |
| GET/PUT | store/customers/me/ |
β JWT | View/update own profile |
| GET | store/customers/{id}/history/ |
β Permission | Customer order history |
| POST | auth/users/ |
No | Register new user |
| POST | auth/jwt/create/ |
No | Get access + refresh tokens |
| POST | auth/jwt/refresh/ |
No | Refresh access token |
| GET | auth/users/me/ |
β JWT | Current user info |
store/products/?collection_id=3
store/products/?unit_price__gt=20&unit_price__lt=100
store/products/?search=coffee
store/products/?ordering=-unit_price
store/products/?page=2
POST /auth/users/ β Register
POST /auth/jwt/create/ β Get JWT token
Authorization: JWT <token> β Use in every protected request
POST /auth/jwt/refresh/ β Refresh expired token
JWT config:
SIMPLE_JWT = {
'AUTH_HEADER_TYPES': ('JWT',),
'ACCESS_TOKEN_LIFETIME': timedelta(days=3)
}# playground/tasks.py
@shared_task
def notify_customers(message):
print('Sending 10k emails...')
sleep(10)
print('Emails were successfully sent!')Scheduled via Celery Beat:
CELERY_BEAT_SCHEDULE = {
'notify_customers': {
'task': 'playground.tasks.notify_customers',
'schedule': crontab(day_of_week=1, hour=7, minute=30),
'args': ['Hello, customers!']
}
}Monitor at Flower dashboard β localhost:5555
git clone https://github.com/ays19/storefront.git
cd storefront
chmod +x docker-entrypoint.sh wait-for-it.sh
docker-compose up --buildServices started:
| Service | URL |
|---|---|
| Django API | http://localhost:8000 |
| Admin Panel | http://localhost:8000/admin |
| Flower (Celery) | http://localhost:5555 |
| smtp4dev (Email) | http://localhost:5000 |
| Silk (Profiler) | http://localhost:8000/silk |
Default superuser: admin / admin1234
pytestlocust -f locustfiles/browse_products.pyOpen http://localhost:8089 and simulate concurrent users hitting:
- Product list with collection filters
- Individual product detail pages
- Add-to-cart operations
Task weights reflect realistic traffic β product detail views are 4x more frequent than list views.
Every request is profiled in development. Access at /silk/ to inspect:
- SQL query count per request
- Slow query detection
- Request/response timeline
Example optimization caught by Silk:
# Before β N+1 queries (11 queries for 10 products)
queryset = Product.objects.all()
# After β 2 queries total
queryset = Product.objects.prefetch_related('images').all()Deployed on Railway using Nixpacks (no Dockerfile needed in production).
# railway.toml
[build]
builder = "nixpacks"
buildCommand = "pip install -r requirements.txt && python manage.py collectstatic --noinput"
[deploy]
startCommand = "gunicorn storefront.wsgi:application --bind 0.0.0.0:$PORT --workers 2"
releaseCommand = "python manage.py migrate && python manage.py create_superuser_env"Every push to main triggers an automatic deploy.
Ahsan Yasir Sharar
Backend Engineer β Django Β· REST APIs Β· System Design
β Star this repo if you found it useful.