{"id":13416193,"url":"https://github.com/HackSoftware/Django-Styleguide","last_synced_at":"2025-03-14T23:31:25.513Z","repository":{"id":38815516,"uuid":"142351241","full_name":"HackSoftware/Django-Styleguide","owner":"HackSoftware","description":"Django styleguide used in HackSoft projects","archived":false,"fork":false,"pushed_at":"2025-02-19T08:27:31.000Z","size":512,"stargazers_count":5355,"open_issues_count":2,"forks_count":545,"subscribers_count":122,"default_branch":"master","last_synced_at":"2025-03-13T08:48:15.633Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/HackSoftware.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-07-25T20:35:40.000Z","updated_at":"2025-03-13T00:07:12.000Z","dependencies_parsed_at":"2023-02-17T20:46:00.283Z","dependency_job_id":"351f3f87-ba20-4f05-80cf-b7685a8ed01c","html_url":"https://github.com/HackSoftware/Django-Styleguide","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/HackSoftware%2FDjango-Styleguide","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/HackSoftware%2FDjango-Styleguide/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/HackSoftware%2FDjango-Styleguide/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/HackSoftware%2FDjango-Styleguide/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/HackSoftware","download_url":"https://codeload.github.com/HackSoftware/Django-Styleguide/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243663500,"owners_count":20327299,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-07-30T21:00:55.292Z","updated_at":"2025-03-14T23:31:25.504Z","avatar_url":"https://github.com/HackSoftware.png","language":"Python","readme":"# Django Styleguide\n\n\u003e 👀 **Need help with your Django project?** [HackSoft is here for you](https://www.hacksoft.io/solutions/django?utm_source=django-styleguide\u0026utm_medium=web\u0026utm_campaign=Django-Campaign). Reach out at `consulting@hacksoft.io`\n\n![Django Styleguide](logo.png)\n\n**Table of contents:**\n\n\u003c!-- toc --\u003e\n\n- [How to ask a question or propose something?](#how-to-ask-a-question-or-propose-something)\n- [What is this?](#what-is-this)\n- [How to use it?](#how-to-use-it)\n- [Overview](#overview)\n- [Why not?](#why-not)\n- [Cookie Cutter](#cookie-cutter)\n- [Models](#models)\n  - [Base model](#base-model)\n  - [Validation - `clean` and `full_clean`](#validation---clean-and-full_clean)\n  - [Validation - constraints](#validation---constraints)\n  - [Properties](#properties)\n  - [Methods](#methods)\n  - [Testing](#testing)\n- [Services](#services)\n  - [Example - function-based service](#example---function-based-service)\n  - [Example - class-based service](#example---class-based-service)\n  - [Naming convention](#naming-convention)\n  - [Modules](#modules)\n  - [Selectors](#selectors)\n  - [Testing](#testing-1)\n- [APIs \u0026 Serializers](#apis--serializers)\n  - [Naming convention](#naming-convention-1)\n  - [Class-based vs. Function-based](#class-based-vs-function-based)\n  - [List APIs](#list-apis)\n    - [Plain](#plain)\n    - [Filters + Pagination](#filters--pagination)\n  - [Detail API](#detail-api)\n  - [Create API](#create-api)\n  - [Update API](#update-api)\n  - [Fetching objects](#fetching-objects)\n  - [Nested serializers](#nested-serializers)\n  - [Advanced serialization](#advanced-serialization)\n- [Urls](#urls)\n- [Settings](#settings)\n  - [Prefixing environment variables with `DJANGO_`](#prefixing-environment-variables-with-django_)\n  - [Integrations](#integrations)\n  - [Reading from `.env`](#reading-from-env)\n- [Errors \u0026 Exception Handling](#errors--exception-handling)\n  - [How exception handling works (in the context of DRF)](#how-exception-handling-works-in-the-context-of-drf)\n    - [DRF's `ValidationError`](#drfs-validationerror)\n    - [Django's `ValidationError`](#djangos-validationerror)\n  - [Describe how your API errors are going to look like.](#describe-how-your-api-errors-are-going-to-look-like)\n  - [Know how to change the default exception handling behavior.](#know-how-to-change-the-default-exception-handling-behavior)\n  - [Approach 1 - Use DRF's default exceptions, with very little modifications.](#approach-1---use-drfs-default-exceptions-with-very-little-modifications)\n  - [Approach 2 - HackSoft's proposed way](#approach-2---hacksofts-proposed-way)\n  - [More ideas](#more-ideas)\n- [Testing](#testing-2)\n  - [Overview](#overview-1)\n  - [Naming conventions](#naming-conventions)\n  - [Factories](#factories)\n- [Celery](#celery)\n  - [The basics](#the-basics)\n  - [Error handling](#error-handling)\n  - [Configuration](#configuration)\n  - [Structure](#structure)\n  - [Periodic Tasks](#periodic-tasks)\n  - [Beyond](#beyond)\n- [Cookbook](#cookbook)\n  - [Handling updates with a service](#handling-updates-with-a-service)\n- [DX (Developer Experience)](#dx-developer-experience)\n  - [`mypy` / type annotations](#mypy--type-annotations)\n- [Django Styleguide in the Wild](#django-styleguide-in-the-wild)\n- [Additional resources / Alternatives](#additional-resources--alternatives)\n- [Inspiration](#inspiration)\n\n\u003c!-- tocstop --\u003e\n\n## How to ask a question or propose something?\n\nFew points to navigate yourself:\n\n1. If you've read the Django Styleguide \u0026 you have questions or suggestions, **the simplest thing you can is to open an issue.** We will respond.\n1. Even if you have a question that you are not sure if it's related to the Django Styleguide - **just open an issue anyway.** We will respond.\n1. **If you want to see a code example**, make sure to head to the [Django Styleguide Example](https://github.com/HackSoftware/Django-Styleguide-Example) repository. We treat this as a \"Django test project\", combining best practices \u0026 also [examples from our blog](https://www.hacksoft.io/blog).\n\nThat's about it ✨\n\n## What is this?\n\nHello 👋\n\nThis is the Django Styleguide, created by us, the folks at [HackSoft](https://hacksoft.io).\n\n**Few important notes about it:**\n\n1. It's derived from many years of experience \u0026 many Django projects, both big \u0026 small.\n1. It's pragmatic. All things mentioned here are things tested in production.\n1. It's opinionated. This is how we build applications with Django.\n1. It's not the only way. There are other ways of building \u0026 structuring Django projects that can do the job for you.\n1. We have a [`Django-Styleguide-Example`](https://github.com/HackSoftware/Django-Styleguide-Example) to show most of the styleguide in an actual project.\n\n**You can watch Radoslav Georgiev's [Django structure for scale and longevity](https://www.youtube.com/watch?v=yG3ZdxBb1oo) for the philosophy behind the styleguide:**\n\n[![Django structure for scale and longevity by Radoslav Georgiev](https://img.youtube.com/vi/yG3ZdxBb1oo/0.jpg)](https://www.youtube.com/watch?v=yG3ZdxBb1oo)\n\n**You can also watch Radoslav Georgiev \u0026 Ivaylo Bachvarov's [discussion on HackCast, around the Django Styleguide](https://www.youtube.com/watch?v=9VfRaPECbpY):**\n\n[![HackCast S02E08 - Django Community \u0026 Django Styleguide](https://img.youtube.com/vi/9VfRaPECbpY/0.jpg)](https://www.youtube.com/watch?v=9VfRaPECbpY)\n\n## How to use it?\n\nWhen it comes to the Django Styleguide, **there are 3 general ways of using it:**\n\n1. Strictly follow everything written here.\n2. Cherry-pick whatever makes sense to you, based on your specific context.\n3. Don't follow anything written here.\n\n**We recommend point number 2:**\n\n- Read the styleguide.\n- Decide what's going to work best for you.\n- Adapt for your specific case.\n\n## Overview\n\nThe core of the Django Styleguide can be summarized as follows:\n\n**In Django, business logic should live in:**\n\n- Services - functions, that mostly take care of writing things to the database.\n- Selectors - functions, that mostly take care of fetching things from the database.\n- Model properties (with some exceptions).\n- Model `clean` method for additional validations (with some exceptions).\n\n**In Django, business logic should not live in:**\n\n- APIs and Views.\n- Serializers and Forms.\n- Form tags.\n- Model `save` method.\n- Custom managers or querysets.\n- Signals.\n\n**Model properties vs selectors:**\n\n- If the property spans multiple relations, it should better be a selector.\n- If the property is non-trivial \u0026 can easily cause `N + 1` queries problem, when serialized, it should better be a selector.\n\nThe general idea is to \"separate concerns\" so those concerns can be maintainable / testable.\n\n## Why not?\n\n\u003e 🤔 Why not put your business logic in APIs / Views / Serializers / Forms?\n\nRelying on generic APIs / Views, with the combination of serializers \u0026 forms does 2 major things:\n\n1. Fragments the business logic in multiple places, making it really hard to trace the data flow.\n2. Hides things from you. In order to change something, you need to know the inner-workings of the abstraction that you are using.\n\nGeneric APIs \u0026 Views, in combination with serializers \u0026 forms, is really great for the straightforward \"CRUD for a model\" case.\n\nFrom our experience, so far, this straightforward case rarely happens. **And once you leave the happy CRUD path, things start to get messy.**\n\nAnd once things start to get messy, you need more \"boxes\", to organize your code in a better way.\n\nThis styleguide aims to:\n\n1. Give you those \"boxes\".\n1. Help you figure out your own \"boxes\", for your own specific context \u0026 needs.\n\n---\n\n\u003e 🤔 Why not put your business logic in custom managers and/or querysets?\n\nThis is actually a good idea \u0026 you might introduce custom managers \u0026 querysets, that can expose better API, tailored to your domain.\n\nBut trying to place all of your business logic in a custom manager is not a great idea, because of the following:\n\n1. Business logic has its own domain, which is not always directly mapped to your data model (models)\n1. Business logic most often spans across multiple models, so it's really hard to choose where to place something.\n   - Let's say you have a custom piece of logic that touches models `A`, `B`, `C`, and `D`. Where do you put it?\n1. There can be additional calls to 3rd party systems. You don't want those in your custom manager methods.\n\n**The idea is to let your domain live separately from your data model \u0026 API layer.**\n\nIf we take the idea of having custom queryset/managers and combine that with the idea of letting the domain live separately, we'll end up with what we call a \"service layer\".\n\n**Services can be functions, classes, modules, or whatever makes sense for your particular case.**\n\nWith all that in mind, custom managers \u0026 querysets are very powerful tools and should be used to expose better interfaces for your models.\n\n---\n\n\u003e 🤔 Why not put your business logic in signals?\n\nFrom all of the available options, perhaps, this one will lead you to a very bad place very quickly:\n\n1. Signals are a great tool for **connecting things that should not know about each other, yet, you want them to be connected.**\n1. Signals are also a great tool **for handling cache invalidation** outside your business logic layer.\n1. If we start using signals for things that are heavily connected, we are just making the connection more implicit and making it harder to trace the data flow.\n\nThat's why we recommend using signals for very particular use cases, but generally, **we don't recommend using them for structuring the domain / business layer.**\n\n## Cookie Cutter\n\nWe recommend starting every new project with some kind of cookiecutter. Having the proper structure from the start pays off.\n\nFew examples:\n\n- You can use the [`Styleguide-Example`](https://github.com/HackSoftware/Styleguide-Example) project as a starting point.\n- You can also use [`cookiecutter-django`](https://github.com/pydanny/cookiecutter-django) since it has a ton of good stuff inside.\n- Or you can create something that works for your case \u0026 turn it into a [cookiecutter](https://cookiecutter.readthedocs.io/en/latest/) project.\n\n## Models\n\nModels should take care of the data model and not much else.\n\n### Base model\n\nIt's a good idea to define a `BaseModel`, that you can inherit.\n\nUsually, fields like `created_at` and `updated_at` are perfect candidates to go into a `BaseModel`.\n\nHere's an example `BaseModel`:\n\n```python\nfrom django.db import models\nfrom django.utils import timezone\n\n\nclass BaseModel(models.Model):\n    created_at = models.DateTimeField(db_index=True, default=timezone.now)\n    updated_at = models.DateTimeField(auto_now=True)\n\n    class Meta:\n        abstract = True\n```\n\nThen, whenever you need a new model, just inherit `BaseModel`:\n\n```python\nclass SomeModel(BaseModel):\n    pass\n```\n\n### Validation - `clean` and `full_clean`\n\nLets take a look at an example model:\n\n```python\nclass Course(BaseModel):\n    name = models.CharField(unique=True, max_length=255)\n\n    start_date = models.DateField()\n    end_date = models.DateField()\n\n    def clean(self):\n        if self.start_date \u003e= self.end_date:\n            raise ValidationError(\"End date cannot be before start date\")\n```\n\nWe are defining the model's `clean` method, because we want to make sure we get good data in our database.\n\nNow, in order for the `clean` method to be called, someone must call `full_clean` on an instance of our model, before saving.\n\n**Our recommendation is to do that in the service, right before calling save:**\n\n```python\ndef course_create(*, name: str, start_date: date, end_date: date) -\u003e Course:\n    obj = Course(name=name, start_date=start_date, end_date=end_date)\n\n    obj.full_clean()\n    obj.save()\n\n    return obj\n```\n\nThis also plays well with Django admin, because the forms used there will trigger `full_clean` on the instance.\n\n**We have few general rules of thumb for when to add validation in the model's `clean` method:**\n\n1. If we are validating based on multiple, **non-relational fields**, of the model.\n1. If the validation itself is simple enough.\n\n**Validation should be moved to the service layer if:**\n\n1. The validation logic is more complex.\n1. Spanning relations \u0026 fetching additional data is required.\n\n\u003e It's OK to have validation both in `clean` and in the service, but we tend to move things in the service, if that's the case.\n\n### Validation - constraints\n\nAs proposed in [this issue](https://github.com/HackSoftware/Django-Styleguide/issues/22), if you can do validation using [Django's constraints](https://docs.djangoproject.com/en/dev/ref/models/constraints/), then you should aim for that.\n\nLess code to write, less code to maintain, the database will take care of the data even if it's being inserted from a different place.\n\nLets look at an example!\n\n```python\nclass Course(BaseModel):\n    name = models.CharField(unique=True, max_length=255)\n\n    start_date = models.DateField()\n    end_date = models.DateField()\n\n    class Meta:\n        constraints = [\n            models.CheckConstraint(\n                name=\"start_date_before_end_date\",\n                check=Q(start_date__lt=F(\"end_date\"))\n            )\n        ]\n```\n\nNow, if we try to create new object via `course.save()` or via `Course.objects.create(...)`, we are going to get an `IntegrityError`, rather than a `ValidationError`.\n\nThis can actually be a downside (_this is not the case, starting from Django 4.1. Check the extra section below._) to the approach, because now, we have to deal with the `IntegrityError`, which does not always have the best error message.\n\n\u003e 👀 ⚠️ 👀 Since Django 4.1, calling `.full_clean` will also check model constraints!\n\u003e\n\u003e This actually removes the downside, mentioned above, since you'll get a nice `ValidationError`, if your model constraints fail the check (if you go thru `Model.objects.create(...)` the downside still holds)\n\u003e\n\u003e More on this, here - \u003chttps://docs.djangoproject.com/en/4.1/ref/models/instances/#validating-objects\u003e\n\u003e\n\u003e For an example test case, check the Styleguide-Example repo - \u003chttps://github.com/HackSoftware/Django-Styleguide-Example/blob/master/styleguide_example/common/tests/models/test_random_model.py#L12\u003e\n\nThe Django's documentation on constraints is quite lean, so you can check the following articles by Adam Johnson, for examples of how to use them:\n\n1. [Using Django Check Constraints to Ensure Only One Field Is Set](https://adamj.eu/tech/2020/03/25/django-check-constraints-one-field-set/)\n1. [Django’s Field Choices Don’t Constrain Your Data](https://adamj.eu/tech/2020/01/22/djangos-field-choices-dont-constrain-your-data/)\n1. [Using Django Check Constraints to Prevent Self-Following](https://adamj.eu/tech/2021/02/26/django-check-constraints-prevent-self-following/)\n\n### Properties\n\nModel properties are great way to quickly access a derived value from a model's instance.\n\nFor example, lets look at the `has_started` and `has_finished` properties of our `Course` model:\n\n```python\nfrom django.utils import timezone\nfrom django.core.exceptions import ValidationError\n\n\nclass Course(BaseModel):\n    name = models.CharField(unique=True, max_length=255)\n\n    start_date = models.DateField()\n    end_date = models.DateField()\n\n    def clean(self):\n        if self.start_date \u003e= self.end_date:\n            raise ValidationError(\"End date cannot be before start date\")\n\n    @property\n    def has_started(self) -\u003e bool:\n        now = timezone.now()\n\n        return self.start_date \u003c= now.date()\n\n    @property\n    def has_finished(self) -\u003e bool:\n        now = timezone.now()\n\n        return self.end_date \u003c= now.date()\n```\n\nThose properties are handy, because we can now refer to them in serializers or use them in templates.\n\n**We have few general rules of thumb, for when to add properties to the model:**\n\n1. If we need a simple derived value, based on **non-relational model fields**, add a `@property` for that.\n1. If the calculation of the derived value is simple enough.\n\n**Properties should be something else (service, selector, utility) in the following cases:**\n\n1. If we need to span multiple relations or fetch additional data.\n1. If the calculation is more complex.\n\nKeep in mind that those rules are vague, because context is quite often important. Use your best judgement!\n\n### Methods\n\nModel methods are also very powerful tool, that can build on top of properties.\n\nLets see an example with the `is_within(self, x)` method:\n\n```python\nfrom django.core.exceptions import ValidationError\nfrom django.utils import timezone\n\n\nclass Course(BaseModel):\n    name = models.CharField(unique=True, max_length=255)\n\n    start_date = models.DateField()\n    end_date = models.DateField()\n\n    def clean(self):\n        if self.start_date \u003e= self.end_date:\n            raise ValidationError(\"End date cannot be before start date\")\n\n    @property\n    def has_started(self) -\u003e bool:\n        now = timezone.now()\n\n        return self.start_date \u003c= now.date()\n\n    @property\n    def has_finished(self) -\u003e bool:\n        now = timezone.now()\n\n        return self.end_date \u003c= now.date()\n\n    def is_within(self, x: date) -\u003e bool:\n        return self.start_date \u003c= x \u003c= self.end_date\n```\n\n`is_within` cannot be a property, because it requires an argument. So it's a method instead.\n\nAnother great way for using methods in models is using them for **attribute setting**, when setting one attribute must always be followed by setting another attribute with a derived value.\n\nAn example:\n\n```python\nfrom django.utils.crypto import get_random_string\nfrom django.conf import settings\nfrom django.utils import timezone\n\n\nclass Token(BaseModel):\n    secret = models.CharField(max_length=255, unique=True)\n    expiry = models.DateTimeField(blank=True, null=True)\n\n    def set_new_secret(self):\n        now = timezone.now()\n\n        self.secret = get_random_string(255)\n        self.expiry = now + settings.TOKEN_EXPIRY_TIMEDELTA\n\n        return self\n```\n\nNow, we can safely call `set_new_secret`, that'll produce correct values for both `secret` and `expiry`.\n\n**We have few general rules of thumb, for when to add methods to the model:**\n\n1. If we need a simple derived value, that requires arguments, based on **non-relational model fields**, add a method for that.\n1. If the calculation of the derived value is simple enough.\n1. If setting one attribute always requires setting values to other attributes, use a method for that.\n\n**Methods should be something else (service, selector, utility) in the following cases:**\n\n1. If we need to span multiple relations or fetch additional data.\n1. If the calculation is more complex.\n\nKeep in mind that those rules are vague, because context is quite often important. Use your best judgement!\n\n### Testing\n\nModels need to be tested only if there's something additional to them - like validation, properties or methods.\n\nHere's an example:\n\n```python\nfrom datetime import timedelta\n\nfrom django.test import TestCase\nfrom django.core.exceptions import ValidationError\nfrom django.utils import timezone\n\nfrom project.some_app.models import Course\n\n\nclass CourseTests(TestCase):\n    def test_course_end_date_cannot_be_before_start_date(self):\n        start_date = timezone.now()\n        end_date = timezone.now() - timedelta(days=1)\n\n        course = Course(start_date=start_date, end_date=end_date)\n\n        with self.assertRaises(ValidationError):\n            course.full_clean()\n```\n\nA few things to note here:\n\n1. We assert that a validation error is going to be raised if we call `full_clean`.\n1. **We are not hitting the database at all**, since there's no need for that. This can speed up certain tests.\n\n## Services\n\nServices are where business logic lives.\n\nThe service layer speaks the specific domain language of the software, can access the database \u0026 other resources \u0026 can interact with other parts of your system.\n\nHere's a very simple diagram, positioning the service layer in our Django apps:\n\n![Service layer](https://user-images.githubusercontent.com/387867/134778130-be168592-b953-4b74-8588-a3dbaa0b6871.png)\n\nA service can be:\n\n- A simple function.\n- A class.\n- An entire module.\n- Whatever makes sense in your case.\n\nIn most cases, a service can be simple function that:\n\n- Lives in `\u003cyour_app\u003e/services.py` module.\n- Takes keyword-only arguments, unless it requires no or one argument.\n- Is type-annotated (even if you are not using [`mypy`](https://github.com/python/mypy) at the moment).\n- Interacts with the database, other resources \u0026 other parts of your system.\n- Does business logic - from simple model creation to complex cross-cutting concerns, to calling external services \u0026 tasks.\n\n### Example - function-based service\n\nAn example service that creates a user:\n\n```python\ndef user_create(\n    *,\n    email: str,\n    name: str\n) -\u003e User:\n    user = User(email=email)\n    user.full_clean()\n    user.save()\n\n    profile_create(user=user, name=name)\n    confirmation_email_send(user=user)\n\n    return user\n```\n\nAs you can see, this service calls 2 other services - `profile_create` and `confirmation_email_send`.\n\nIn this example, everything related to the user creation is in one place and can be traced.\n\n### Example - class-based service\n\n**Additionally, we can have \"class-based\" services**, which is a fancy way of saying - wrap the logic in a class.\n\nHere's an example, taken straight from the [Django Styleguide Example](https://github.com/HackSoftware/Django-Styleguide-Example/blob/master/styleguide_example/files/services.py#L22), related to file upload:\n\n```python\n# https://github.com/HackSoftware/Django-Styleguide-Example/blob/master/styleguide_example/files/services.py\n\n\nclass FileStandardUploadService:\n    \"\"\"\n    This also serves as an example of a service class,\n    which encapsulates 2 different behaviors (create \u0026 update) under a namespace.\n\n    Meaning, we use the class here for:\n\n    1. The namespace\n    2. The ability to reuse `_infer_file_name_and_type` (which can also be an util)\n    \"\"\"\n    def __init__(self, user: BaseUser, file_obj):\n        self.user = user\n        self.file_obj = file_obj\n\n    def _infer_file_name_and_type(self, file_name: str = \"\", file_type: str = \"\") -\u003e Tuple[str, str]:\n        file_name = file_name or self.file_obj.name\n\n        if not file_type:\n            guessed_file_type, encoding = mimetypes.guess_type(file_name)\n            file_type = guessed_file_type or \"\"\n\n        return file_name, file_type\n\n    @transaction.atomic\n    def create(self, file_name: str = \"\", file_type: str = \"\") -\u003e File:\n        _validate_file_size(self.file_obj)\n\n        file_name, file_type = self._infer_file_name_and_type(file_name, file_type)\n\n        obj = File(\n            file=self.file_obj,\n            original_file_name=file_name,\n            file_name=file_generate_name(file_name),\n            file_type=file_type,\n            uploaded_by=self.user,\n            upload_finished_at=timezone.now()\n        )\n\n        obj.full_clean()\n        obj.save()\n\n        return obj\n\n    @transaction.atomic\n    def update(self, file: File, file_name: str = \"\", file_type: str = \"\") -\u003e File:\n        _validate_file_size(self.file_obj)\n\n        file_name, file_type = self._infer_file_name_and_type(file_name, file_type)\n\n        file.file = self.file_obj\n        file.original_file_name = file_name\n        file.file_name = file_generate_name(file_name)\n        file.file_type = file_type\n        file.uploaded_by = self.user\n        file.upload_finished_at = timezone.now()\n\n        file.full_clean()\n        file.save()\n\n        return file\n```\n\nAs stated in the comment, we are using this approach for 2 main reasons:\n\n1. **Namespace.** We have a single namespace for our create \u0026 update.\n1. **Reuse** of the `_infer_file_name_and_type` logic.\n\nHere's how this service is used:\n\n```python\n# https://github.com/HackSoftware/Django-Styleguide-Example/blob/master/styleguide_example/files/apis.py\n\nclass FileDirectUploadApi(ApiAuthMixin, APIView):\n    def post(self, request):\n        service = FileDirectUploadService(\n            user=request.user,\n            file_obj=request.FILES[\"file\"]\n        )\n        file = service.create()\n\n        return Response(data={\"id\": file.id}, status=status.HTTP_201_CREATED)\n```\n\nAnd\n\n```python\n@admin.register(File)\nclass FileAdmin(admin.ModelAdmin):\n    # ... other code here ...\n    # https://github.com/HackSoftware/Django-Styleguide-Example/blob/master/styleguide_example/files/admin.py\n\n    def save_model(self, request, obj, form, change):\n        try:\n            cleaned_data = form.cleaned_data\n\n            service = FileDirectUploadService(\n                file_obj=cleaned_data[\"file\"],\n                user=cleaned_data[\"uploaded_by\"]\n            )\n\n            if change:\n                service.update(file=obj)\n            else:\n                service.create()\n        except ValidationError as exc:\n            self.message_user(request, str(exc), messages.ERROR)\n```\n\nAdditionally, using class-based services is a good idea for \"flows\" - things that go through multiple steps.\n\nFor example, this service represents a \"direct file upload flow\", with a `start` and `finish` (and additionally):\n\n```python\n# https://github.com/HackSoftware/Django-Styleguide-Example/blob/master/styleguide_example/files/services.py\n\n\nclass FileDirectUploadService:\n    \"\"\"\n    This also serves as an example of a service class,\n    which encapsulates a flow (start \u0026 finish) + one-off action (upload_local) into a namespace.\n\n    Meaning, we use the class here for:\n\n    1. The namespace\n    \"\"\"\n    def __init__(self, user: BaseUser):\n        self.user = user\n\n    @transaction.atomic\n    def start(self, *, file_name: str, file_type: str) -\u003e Dict[str, Any]:\n        file = File(\n            original_file_name=file_name,\n            file_name=file_generate_name(file_name),\n            file_type=file_type,\n            uploaded_by=self.user,\n            file=None\n        )\n        file.full_clean()\n        file.save()\n\n        upload_path = file_generate_upload_path(file, file.file_name)\n\n        \"\"\"\n        We are doing this in order to have an associated file for the field.\n        \"\"\"\n        file.file = file.file.field.attr_class(file, file.file.field, upload_path)\n        file.save()\n\n        presigned_data: Dict[str, Any] = {}\n\n        if settings.FILE_UPLOAD_STORAGE == FileUploadStorage.S3:\n            presigned_data = s3_generate_presigned_post(\n                file_path=upload_path, file_type=file.file_type\n            )\n\n        else:\n            presigned_data = {\n                \"url\": file_generate_local_upload_url(file_id=str(file.id)),\n            }\n\n        return {\"id\": file.id, **presigned_data}\n\n    @transaction.atomic\n    def finish(self, *, file: File) -\u003e File:\n        # Potentially, check against user\n        file.upload_finished_at = timezone.now()\n        file.full_clean()\n        file.save()\n\n        return file\n```\n\n### Naming convention\n\nNaming convention depends on your taste. It pays off to have something consistent throughout a project.\n\nIf we take the example above, our service is named `user_create`. The pattern is - `\u003centity\u003e_\u003caction\u003e`.\n\nThis is what we prefer in HackSoft's projects. This seems odd at first, but it has few nice features:\n\n- **Namespacing.** It's easy to spot all services starting with `user_` and it's a good idea to put them in a `users.py` module.\n- **Greppability.** Or in other words, if you want to see all actions for a specific entity, just grep for `user_`.\n\n### Modules\n\nIf you have a simple-enough Django app with a bunch of services, they can all live happily in the `service.py` module.\n\nBut when things get big, you might want to split `services.py` into a folder with sub-modules, depending on the different sub-domains that you are dealing with in your app.\n\nFor example, lets say we have an `authentication` app, where we have 1 sub-module in our `services` module, that deals with `jwt`, and one sub-module that deals with `oauth`.\n\nThe structure may look like this:\n\n```\nservices\n├── __init__.py\n├── jwt.py\n└── oauth.py\n```\n\nThere are lots of flavors here:\n\n- You can do the import-export dance in `services/__init__.py`, so you can import from `project.authentication.services` everywhere else\n- You can create a folder-module, `jwt/__init__.py`, and put the code there.\n- Basically, the structure is up to you. If you feel it's time to restructure and refactor - do so.\n\n### Selectors\n\nIn most of our projects, we distinguish between \"Pushing data to the database\" and \"Pulling data from the database\":\n\n1. Services take care of the push.\n1. **Selectors take care of the pull.**\n1. Selectors can be viewed as a \"sub-layer\" to services, that's specialized in fetching data.\n\n\u003e If this idea does not resonate well with you, you can just have services for both \"kinds\" of operations.\n\nA selector follows the same rules as a service.\n\nFor example, in a module `\u003cyour_app\u003e/selectors.py`, we can have the following:\n\n```python\ndef user_list(*, fetched_by: User) -\u003e Iterable[User]:\n    user_ids = user_get_visible_for(user=fetched_by)\n\n    query = Q(id__in=user_ids)\n\n    return User.objects.filter(query)\n```\n\nAs you can see, `user_get_visible_for` is another selector.\n\nYou can return querysets, or lists or whatever makes sense to your specific case.\n\n### Testing\n\nSince services hold our business logic, they are an ideal candidate for tests.\n\nIf you decide to cover the service layer with tests, we have few general rules of thumb to follow:\n\n1. The tests **should cover the business logic** in an exhaustive manner.\n1. The tests **should hit the database** - creating \u0026 reading from it.\n1. The tests **should mock async task calls \u0026 everything that goes outside the project.**\n\nWhen creating the required state for a given test, one can use a combination of:\n\n- Fakes (We recommend using [`faker`](https://github.com/joke2k/faker))\n- Other services, to create the required objects.\n- Special test utility \u0026 helper methods.\n- Factories (We recommend using [`factory_boy`](https://factoryboy.readthedocs.io/en/latest/orms.html))\n- Plain `Model.objects.create()` calls, if factories are not yet introduced in the project.\n- Usually, whatever suits you better.\n\n**Let's take a look at our service from the example:**\n\n```python\nfrom django.contrib.auth.models import User\nfrom django.core.exceptions import ValidationError\nfrom django.db import transaction\n\nfrom project.payments.selectors import items_get_for_user\nfrom project.payments.models import Item, Payment\nfrom project.payments.tasks import payment_charge\n\n\n@transaction.atomic\ndef item_buy(\n    *,\n    item: Item,\n    user: User,\n) -\u003e Payment:\n    if item in items_get_for_user(user=user):\n        raise ValidationError(f'Item {item} already in {user} items.')\n\n    payment = Payment(\n        item=item,\n        user=user,\n        successful=False\n    )\n    payment.full_clean()\n    payment.save()\n\n    # Run the task once the transaction has commited,\n    # guaranteeing the object has been created.\n    transaction.on_commit(\n        lambda: payment_charge.delay(payment_id=payment.id)\n    )\n\n    return payment\n```\n\nThe service:\n\n- Calls a selector for validation.\n- Creates an object.\n- Delays a task.\n\n**Those are our tests:**\n\n```python\nfrom unittest.mock import patch, Mock\n\nfrom django.test import TestCase\nfrom django.contrib.auth.models import User\nfrom django.core.exceptions import ValidationError\n\nfrom django_styleguide.payments.services import item_buy\nfrom django_styleguide.payments.models import Payment, Item\n\n\nclass ItemBuyTests(TestCase):\n    @patch('project.payments.services.items_get_for_user')\n    def test_buying_item_that_is_already_bought_fails(\n        self, items_get_for_user_mock: Mock\n    ):\n        \"\"\"\n        Since we already have tests for `items_get_for_user`,\n        we can safely mock it here and give it a proper return value.\n        \"\"\"\n        user = User(username='Test User')\n        item = Item(\n            name='Test Item',\n            description='Test Item description',\n            price=10.15\n        )\n\n        items_get_for_user_mock.return_value = [item]\n\n        with self.assertRaises(ValidationError):\n            item_buy(user=user, item=item)\n\n    @patch('project.payments.services.payment_charge.delay')\n    def test_buying_item_creates_a_payment_and_calls_charge_task(\n        self,\n        payment_charge_mock: Mock\n    ):\n        # How we prepare our tests is a topic for a different discussion\n        user = given_a_user(username=\"Test user\")\n        item = given_a_item(\n            name='Test Item',\n            description='Test Item description',\n            price=10.15\n        )\n\n        self.assertEqual(0, Payment.objects.count())\n\n        payment = item_buy(user=user, item=item)\n\n        self.assertEqual(1, Payment.objects.count())\n        self.assertEqual(payment, Payment.objects.first())\n\n        self.assertFalse(payment.successful)\n\n        payment_charge_mock.assert_called_once()\n```\n\n## APIs \u0026 Serializers\n\nWhen using services \u0026 selectors, all of your APIs should look simple \u0026 identical.\n\n**When we are creating new APIs, we follow those general rules:**\n\n- Have 1 API per operation. This means, for CRUD on a model, having 4 APIs.\n- Inherit from the most simple `APIView` or `GenericAPIView`.\n  - Avoid the more abstract classes, since they tend to manage things via serializers \u0026 we want to do that via services \u0026 selectors.\n- **Don't do business logic in your API.**\n- You can do **object fetching / data manipulation in your APIs** (potentially, you can extract that to somewhere else).\n  - If you are calling `some_service` in your API, you can extract object fetching / data manipulation to `some_service_parse`.\n- Basically, keep the APIs as simple as possible. They are an interface towards your core business logic.\n\nWhen we are talking about APIs, we need a way to deal with data serialization - both incoming \u0026 outgoing data.\n\n**Here are our rules for API serialization:**\n\n- There should be a dedicated **input serializer** \u0026 a dedicated **output serializer**.\n- **Input serializer** takes care of the data coming in.\n- **Output serializer** takes care of the data coming out.\n- In terms of serialization, Use whatever abstraction works for you.\n\n**In case you are using DRF's serializers, here are our rules:**\n\n- Serializer should be **nested in the API** and be named either `InputSerializer` or `OutputSerializer`.\n- Our preference is for both serializers to inherit from the simpler `Serializer` and avoid using `ModelSerializer`\n  - This is a matter of preference and choice. If `ModelSerializer` is working fine for you, use it.\n- If you need a nested serializer, use the `inline_serializer` util.\n- Reuse serializers as little as possible.\n  - Reusing serializers may expose you to unexpected behavior, when something changes in the base serializers.\n\n### Naming convention\n\nFor our APIs we use the following naming convention: `\u003cEntity\u003e\u003cAction\u003eApi`.\n\nHere are few examples: `UserCreateApi`, `UserSendResetPasswordApi`, `UserDeactivateApi`, etc.\n\n### Class-based vs. Function-based\n\n\u003e This is mostly up to personal preferences, since you can achieve the same results with both approaches.\n\nWe have the following preferences:\n\n1. Pick class-based APIS / views by default.\n1. If everyone else preferes \u0026 are comfortable with functions, use function-based APIs / views.\n\nFor us, the added benefits of using classes for APIs / views are the following:\n\n1. You can inherit a `BaseApi` or add mixins.\n   - If you are using function-based APIs / views, you'll need to do the same, but with decorators.\n2. The class creates a namespace where you can nest things (attributes, methods, etc.).\n   - Additional API configuration can be done via class attributes.\n   - In the case of function-based APIs / views, you need to stack decorators.\n\nHere's an example with a class, inheriting a `BaseApi`:\n\n```python\nclass SomeApi(BaseApi):\n    def get(self, request):\n        data = something()\n\n        return Response(data)\n```\n\nHere's an example with a function, using a `base_api` decorator (implementation is based on your needs)\n\n```python\n@base_api([\"GET\"])\ndef some_api(request):\n    data = something()\n    return Response(data)\n```\n\n### List APIs\n\n#### Plain\n\nA dead-simple list API should look like that:\n\n```python\nfrom rest_framework.views import APIView\nfrom rest_framework import serializers\nfrom rest_framework.response import Response\n\nfrom styleguide_example.users.selectors import user_list\nfrom styleguide_example.users.models import BaseUser\n\n\nclass UserListApi(APIView):\n    class OutputSerializer(serializers.Serializer):\n        id = serializers.CharField()\n        email = serializers.CharField()\n\n    def get(self, request):\n        users = user_list()\n\n        data = self.OutputSerializer(users, many=True).data\n\n        return Response(data)\n```\n\n_Keep in mind this API is public by default. Authentication is up to you._\n\n#### Filters + Pagination\n\nAt first glance, this is tricky, since our APIs are inheriting the plain `APIView` from DRF, while filtering and pagination are baked into the generic ones:\n\n1. [DRF Filtering](https://www.django-rest-framework.org/api-guide/filtering/)\n1. [DRF Pagination](https://www.django-rest-framework.org/api-guide/pagination/)\n\nThat's why, we take the following approach:\n\n1. Selectors take care of the actual filtering.\n1. APIs take care of filter parameter serialization.\n1. If you need some of the generic paginations, provided by DRF, the API should take care of that.\n1. If you need a different pagination, or you are implementing it yourself, either add a new layer to handle pagination or let the selector do that for you.\n\n**Let's look at the example, where we rely on pagination, provided by DRF:**\n\n```python\nfrom rest_framework.views import APIView\nfrom rest_framework import serializers\n\nfrom styleguide_example.api.mixins import ApiErrorsMixin\nfrom styleguide_example.api.pagination import get_paginated_response, LimitOffsetPagination\n\nfrom styleguide_example.users.selectors import user_list\nfrom styleguide_example.users.models import BaseUser\n\n\nclass UserListApi(ApiErrorsMixin, APIView):\n    class Pagination(LimitOffsetPagination):\n        default_limit = 1\n\n    class FilterSerializer(serializers.Serializer):\n        id = serializers.IntegerField(required=False)\n        # Important: If we use BooleanField, it will default to False\n        is_admin = serializers.NullBooleanField(required=False)\n        email = serializers.EmailField(required=False)\n\n    class OutputSerializer(serializers.Serializer):\n        id = serializers.CharField()\n        email = serializers.CharField()\n        is_admin = serializers.BooleanField()\n\n    def get(self, request):\n        # Make sure the filters are valid, if passed\n        filters_serializer = self.FilterSerializer(data=request.query_params)\n        filters_serializer.is_valid(raise_exception=True)\n\n        users = user_list(filters=filters_serializer.validated_data)\n\n        return get_paginated_response(\n            pagination_class=self.Pagination,\n            serializer_class=self.OutputSerializer,\n            queryset=users,\n            request=request,\n            view=self\n        )\n```\n\nWhen we look at the API, we can identify few things:\n\n1. There's a `FilterSerializer`, which will take care of the query parameters. If we don't do this here, we'll have to do it elsewhere \u0026 DRF serializers are great at this job.\n1. We pass the filters to the `user_list` selector\n1. We use the `get_paginated_response` utility, to return a .. paginated response.\n\nNow, let's look at the selector:\n\n```python\nimport django_filters\n\nfrom styleguide_example.users.models import BaseUser\n\n\nclass BaseUserFilter(django_filters.FilterSet):\n    class Meta:\n        model = BaseUser\n        fields = ('id', 'email', 'is_admin')\n\n\ndef user_list(*, filters=None):\n    filters = filters or {}\n\n    qs = BaseUser.objects.all()\n\n    return BaseUserFilter(filters, qs).qs\n```\n\nAs you can see, we are leveraging the powerful [`django-filter`](https://django-filter.readthedocs.io/en/stable/) library.\n\n\u003e 👀 The key thing here is that the selector is responsible for the filtering. You can always use something else, as a filtering abstraction. For most of the cases, `django-filter` is more than enough.\n\nFinally, let's look at `get_paginated_response`:\n\n```python\nfrom rest_framework.response import Response\n\n\ndef get_paginated_response(*, pagination_class, serializer_class, queryset, request, view):\n    paginator = pagination_class()\n\n    page = paginator.paginate_queryset(queryset, request, view=view)\n\n    if page is not None:\n        serializer = serializer_class(page, many=True)\n        return paginator.get_paginated_response(serializer.data)\n\n    serializer = serializer_class(queryset, many=True)\n\n    return Response(data=serializer.data)\n```\n\nThis is basically a code, extracted from within DRF.\n\nSame goes for the `LimitOffsetPagination`:\n\n```python\nfrom collections import OrderedDict\n\nfrom rest_framework.pagination import LimitOffsetPagination as _LimitOffsetPagination\nfrom rest_framework.response import Response\n\n\nclass LimitOffsetPagination(_LimitOffsetPagination):\n    default_limit = 10\n    max_limit = 50\n\n    def get_paginated_data(self, data):\n        return OrderedDict([\n            ('limit', self.limit),\n            ('offset', self.offset),\n            ('count', self.count),\n            ('next', self.get_next_link()),\n            ('previous', self.get_previous_link()),\n            ('results', data)\n        ])\n\n    def get_paginated_response(self, data):\n        \"\"\"\n        We redefine this method in order to return `limit` and `offset`.\n        This is used by the frontend to construct the pagination itself.\n        \"\"\"\n        return Response(OrderedDict([\n            ('limit', self.limit),\n            ('offset', self.offset),\n            ('count', self.count),\n            ('next', self.get_next_link()),\n            ('previous', self.get_previous_link()),\n            ('results', data)\n        ]))\n```\n\nWhat we basically did is reverse-engineered the generic APIs.\n\n\u003e 👀 Again, if you need something else for pagination, you can always implement it \u0026 use it in the same manner. There are cases, where the selector needs to take care of the pagination. We approach those cases the same way we approach filtering.\n\nYou can find the code for the example list API with filters \u0026 pagination in the [Styleguide Example](https://github.com/HackSoftware/Styleguide-Example#example-list-api) project.\n\n### Detail API\n\nHere's an example:\n\n```python\nclass CourseDetailApi(SomeAuthenticationMixin, APIView):\n    class OutputSerializer(serializers.Serializer):\n        id = serializers.CharField()\n        name = serializers.CharField()\n        start_date = serializers.DateField()\n        end_date = serializers.DateField()\n\n    def get(self, request, course_id):\n        course = course_get(id=course_id)\n\n        serializer = self.OutputSerializer(course)\n\n        return Response(serializer.data)\n```\n\n### Create API\n\nHere's an example:\n\n```python\nclass CourseCreateApi(SomeAuthenticationMixin, APIView):\n    class InputSerializer(serializers.Serializer):\n        name = serializers.CharField()\n        start_date = serializers.DateField()\n        end_date = serializers.DateField()\n\n    def post(self, request):\n        serializer = self.InputSerializer(data=request.data)\n        serializer.is_valid(raise_exception=True)\n\n        course_create(**serializer.validated_data)\n\n        return Response(status=status.HTTP_201_CREATED)\n```\n\n### Update API\n\nHere's an example:\n\n```python\nclass CourseUpdateApi(SomeAuthenticationMixin, APIView):\n    class InputSerializer(serializers.Serializer):\n        name = serializers.CharField(required=False)\n        start_date = serializers.DateField(required=False)\n        end_date = serializers.DateField(required=False)\n\n    def post(self, request, course_id):\n        serializer = self.InputSerializer(data=request.data)\n        serializer.is_valid(raise_exception=True)\n\n        course_update(course_id=course_id, **serializer.validated_data)\n\n        return Response(status=status.HTTP_200_OK)\n```\n\n### Fetching objects\n\nWhen our APIs receive an `object_id`, the question that arises is: **Where should we fetch that object?**\n\nWe have several options:\n\n1. We can pass that object to a serializer, which has a [`PrimaryKeyRelatedField`](https://www.django-rest-framework.org/api-guide/relations/#primarykeyrelatedfield) (or a [`SlugRelatedField`](https://www.django-rest-framework.org/api-guide/relations/#slugrelatedfield) for that matter)\n1. We can do some kind of object fetching in the API \u0026 pass the object to a service or a selector.\n1. We can pass the id to the service / selector and do the object fetching there.\n\nWhat approach we take is a matter of project context \u0026 preference.\n\nWhat we usually do is to fetch objects on the API level, using a special `get_object` util:\n\n```python\ndef get_object(model_or_queryset, **kwargs):\n    \"\"\"\n    Reuse get_object_or_404 since the implementation supports both Model \u0026\u0026 queryset.\n    Catch Http404 \u0026 return None\n    \"\"\"\n    try:\n        return get_object_or_404(model_or_queryset, **kwargs)\n    except Http404:\n        return None\n```\n\nThis is a very basic utility, that handles the exception and returns `None` instead.\n\nWhatever you do, make sure to keep it consistent.\n\n### Nested serializers\n\nIn case you need to use a nested serializer, you can do the following thing:\n\n```python\nclass Serializer(serializers.Serializer):\n    weeks = inline_serializer(many=True, fields={\n        'id': serializers.IntegerField(),\n        'number': serializers.IntegerField(),\n    })\n```\n\nThe implementation of `inline_serializer` can be found [here](https://github.com/HackSoftware/Django-Styleguide-Example/blob/master/styleguide_example/api/utils.py), in the [Styleguide-Example](https://github.com/HackSoftware/Styleguide-Example) repo.\n\n### Advanced serialization\n\nSometimes, the end result of an API can be quite complex. Sometimes, we want to optimize the queries that we do and the optimization itself can be quite complex.\n\nTrying to stick with just an `OutputSerializer` in that case might limit our options.\n\nIn those cases, we can implement our output serialization as a function, and have the optimizations we need there, **instead of having all the optimizations in the selector.**\n\nLets take this API as an example:\n\n```python\nclass SomeGenericFeedApi(BaseApi):\n    def get(self, request):\n        feed = some_feed_get(\n            user=request.user,\n        )\n\n        data = some_feed_serialize(feed)\n\n        return Response(data)\n```\n\nIn this scenario, `some_feed_get` has the responsibility of returning a list of feed items (can be ORM objects, can be just IDs, can be whatever works for you).\n\nAnd we want to push the complexity of serializing this feed, in an optimal manner, to the serializer function - `some_feed_serialize`.\n\nThis means we don't have to do any unnecessary prefetches \u0026 optimizations in `some_feed_get`.\n\nHere's an example of `some_feed_serialize`:\n\n```python\nclass FeedItemSerializer(serializers.Serializer):\n    ... some fields here ...\n    calculated_field = serializers.IntegerField(source=\"_calculated_field\")\n\n\ndef some_feed_serialize(feed: List[FeedItem]):\n    feed_ids = [feed_item.id for feed_item in feed]\n\n    # Refetch items with more optimizations\n    # Based on the relations that are going in\n    objects = FeedItem.objects.select_related(\n      # ... as complex as you want ...\n    ).prefetch_related(\n      # ... as complex as you want ...\n    ).filter(\n      id__in=feed_ids\n    ).order_by(\n      \"-some_timestamp\"\n    )\n\n    some_cache = get_some_cache(feed_ids)\n\n    result = []\n\n    for feed_item in objects:\n        # An example, adding additional fields for the serializer\n        # That are based on values outside of our current object\n        # This may be some optimization to save queries\n        feed_item._calculated_field = some_cache.get(feed_item.id)\n\n        result.append(FeedItemSerializer(feed_item).data)\n\n    return result\n```\n\nAs you can see, this is a pretty generic example, but the idea is simple:\n\n1. Refetch your data, with the needed joins \u0026 prefetches.\n1. Fetch or build in-memory caches, that will save you queries for specific computed values.\n1. Return a result, that's ready to be an API response.\n\nEven though this is labeled as \"advanced serialization\", the pattern is really powerful and can be used for all serializations.\n\nSuch serializer functions usually live in a `serializers.py` module, in the corresponding Django app.\n\n## Urls\n\nWe usually organize our urls the same way we organize our APIs - 1 url per API, meaning 1 url per action.\n\nA general rule of thumb is to split urls from different domains in their own `domain_patterns` list \u0026 include from `urlpatterns`.\n\nHere's an example with the APIs from above:\n\n```python\nfrom django.urls import path, include\n\nfrom project.education.apis import (\n    CourseCreateApi,\n    CourseUpdateApi,\n    CourseListApi,\n    CourseDetailApi,\n    CourseSpecificActionApi,\n)\n\n\ncourse_patterns = [\n    path('', CourseListApi.as_view(), name='list'),\n    path('\u003cint:course_id\u003e/', CourseDetailApi.as_view(), name='detail'),\n    path('create/', CourseCreateApi.as_view(), name='create'),\n    path('\u003cint:course_id\u003e/update/', CourseUpdateApi.as_view(), name='update'),\n    path(\n        '\u003cint:course_id\u003e/specific-action/',\n        CourseSpecificActionApi.as_view(),\n        name='specific-action'\n    ),\n]\n\nurlpatterns = [\n    path('courses/', include((course_patterns, 'courses'))),\n]\n```\n\n**Splitting urls like that can give you the extra flexibility to move separate domain patterns to separate modules**, especially for really big projects, where you'll often have merge conflicts in `urls.py`.\n\nNow, if you like to see the entire url tree structure, you can do just that, by not extracting specific variables for the urls that you include.\n\nHere's an example from our [Django Styleguide Example](https://github.com/HackSoftware/Django-Styleguide-Example/blob/master/styleguide_example/files/urls.py):\n\n```python\nfrom django.urls import path, include\n\nfrom styleguide_example.files.apis import (\n    FileDirectUploadApi,\n\n    FilePassThruUploadStartApi,\n    FilePassThruUploadFinishApi,\n    FilePassThruUploadLocalApi,\n)\n\n\nurlpatterns = [\n    path(\n        \"upload/\",\n        include(([\n            path(\n                \"direct/\",\n                FileDirectUploadApi.as_view(),\n                name=\"direct\"\n            ),\n            path(\n                \"pass-thru/\",\n                include(([\n                    path(\n                        \"start/\",\n                        FilePassThruUploadStartApi.as_view(),\n                        name=\"start\"\n                    ),\n                    path(\n                        \"finish/\",\n                        FilePassThruUploadFinishApi.as_view(),\n                        name=\"finish\"\n                    ),\n                    path(\n                        \"local/\u003cstr:file_id\u003e/\",\n                        FilePassThruUploadLocalApi.as_view(),\n                        name=\"local\"\n                    )\n                ], \"pass-thru\"))\n            )\n        ], \"upload\"))\n    )\n]\n```\n\nSome people prefer the first way of doing it, others prefer the visible tree-like structure. This is up to you \u0026 your team.\n\n## Settings\n\nWhen it comes to Django settings, we tend to follow the folder structure from [`cookiecutter-django`](https://github.com/cookiecutter/cookiecutter-django), with few adjustments:\n\n- We separate Django specific settings from other settings.\n- Everything should be included in `base.py`.\n  - There should be nothing that's only included in `production.py`.\n  - Things that need to only work in production are controlled via environment variables.\n\nHere's the folder structure of our [`Styleguide-Example`](https://github.com/HackSoftware/Styleguide-Example) project:\n\n```\nconfig\n├── __init__.py\n├── django\n│   ├── __init__.py\n│   ├── base.py\n│   ├── local.py\n│   ├── production.py\n│   └── test.py\n├── settings\n│   ├── __init__.py\n│   ├── celery.py\n│   ├── cors.py\n│   ├── sentry.py\n│   └── sessions.py\n├── urls.py\n├── env.py\n└── wsgi.py\n├── asgi.py\n```\n\nIn `config/django`, we put everything that's Django related:\n\n- `base.py` contains most of the settings \u0026 imports everything else from `config/settings`\n- `production.py` imports from `base.py` and then overwrites some specific settings for production.\n- `test.py` imports from `base.py` and then overwrites some specific settings for running tests.\n  - This should be used as the settings module in `pytest.ini`.\n- `local.py` imports from `base.py` and can overwrite some specific settings for local development.\n  - If you want to use that, point to `local` in `manage.py`. Otherwise stick with `base.py`\n\nIn `config/settings`, we put everything else:\n\n- Celery configuration.\n- 3rd party configurations.\n- etc.\n\nThis gives you a nice separation of modules.\n\nAdditionally, we usually have `config/env.py` with the following code:\n\n```python\nimport environ\n\nenv = environ.Env()\n```\n\nAnd then, whenever we need to read something from the environment, we import like that:\n\n```python\nfrom config.env import env\n```\n\nUsually, at the end of the `base.py` module, we import everything from `config/settings`:\n\n```python\nfrom config.settings.cors import *  # noqa\nfrom config.settings.sessions import *  # noqa\nfrom config.settings.celery import *  # noqa\nfrom config.settings.sentry import *  # noqa\n```\n\n### Prefixing environment variables with `DJANGO_`\n\nIn a lot of examples, you'll see that environment variables are usually prefixed with `DJANGO_`. This is very helpful when there are other applications alongside your Django app that run on the same environment. In that case, prefixing the environment variables with `DJANGO_` helps you to differ which are the environment variables specific to your Django app.\n\nIn HackSoft we do not ususally have several apps running on the same environment. So, we tend to prefix with `DJANGO_` only the Django specific environments \u0026 anything else.\n\nFor example, we would have `DJANGO_SETTINGS_MODULE`, `DJANGO_DEBUG`, `DJANGO_ALLOWED_HOSTS`, `DJANGO_CORS_ORIGIN_WHITELIST` prefixed. We would have `AWS_SECRET_KEY`, `CELERY_BROKER_URL`, `EMAILS_ENABLED` not prefixed.\n\nThis is mostly up to personal preference. **Just make sure you are consistent with that.**\n\n### Integrations\n\nSince everything should be imported in `base.py`, but sometimes we don't want to configure a certain integration for local development, we derived the following approach:\n\n- Integration-specific settings are placed in `config/settings/some_integration.py`\n- There's always a boolean setting called `USE_SOME_INTEGRATION`, which reads from the environment \u0026 defaults to `False`.\n- If the value is `True`, then proceed reading other settings \u0026 failing if things are not present in the environment.\n\nFor example, lets take a look at `config/settings/sentry.py`:\n\n```python\nfrom config.env import env\n\nSENTRY_DSN = env('SENTRY_DSN', default='')\n\nif SENTRY_DSN:\n    import sentry_sdk\n    from sentry_sdk.integrations.django import DjangoIntegration\n    from sentry_sdk.integrations.celery import CeleryIntegration\n\n    # ... we proceed with sentry settings here ...\n    # View the full file here - https://github.com/HackSoftware/Styleguide-Example/blob/master/config/settings/sentry.py\n```\n\n### Reading from `.env`\n\nHaving a local `.env` is a nice way of providing values for your settings.\n\nAnd the good thing is, [`django-environ`](https://django-environ.readthedocs.io/en/latest/) provides you with a way to do that:\n\n```python\n# That's in the beginning of base.py\n\nimport os\n\nfrom config.env import env, environ\n\n# Build paths inside the project like this: os.path.join(BASE_DIR, ...)\nBASE_DIR = environ.Path(__file__) - 3\n\nenv.read_env(os.path.join(BASE_DIR, \".env\"))\n```\n\nNow you can have a `.env` (but it's not required) file in your project root \u0026 place values for your settings there.\n\nThere are 2 things worth mentioning here:\n\n1. Don't put `.env` in your source control, since this will leak credentials.\n2. Rather put an `.env.example` with empty values for everything, so new developers can figure out what's being used.\n\n## Errors \u0026 Exception Handling\n\n\u003e 👀 If you want the code, hop to the `Styleguide-Example` project - \u003chttps://github.com/HackSoftware/Styleguide-Example/blob/master/styleguide_example/api/exception_handlers.py\u003e\n\nErrors \u0026 exception handling is a big topic \u0026 quite often - the details are specific for a given project.\n\nThat's why we'll split things into two - **general guidelines**, followed by some **specific approaches** for error handling.\n\n**Our general guidelines are:**\n\n1. Know how exception handling works (we'll give context for Django Rest Framework).\n1. Describe how your API errors are going to look like.\n1. Know how to change the default exception handling behavior.\n\n**Followed by some specific approaches:**\n\n1. Use DRF's default exceptions, with very little modifications.\n1. HackSoft's proposed approach.\n\nIf you are looking for a standard way to structure your error responses, **check RFC7807** - \u003chttps://datatracker.ietf.org/doc/html/rfc7807\u003e (as proposed here - \u003chttps://github.com/HackSoftware/Django-Styleguide/issues/133\u003e)\n\n### How exception handling works (in the context of DRF)\n\nDRF has an excellent guide on how exceptions are being handled, so make sure to read it first - \u003chttps://www.django-rest-framework.org/api-guide/exceptions/\u003e\n\nAdditionally, here's a neat diagram with an overview of the process:\n\n![Exception handler (1)](https://user-images.githubusercontent.com/387867/142426205-2c0356e6-ce20-425e-a811-072c3334edb0.png)\n\nBasically, if the exception handler cannot handle the given exception \u0026 returns `None`, this will result in an unhandled exception \u0026 a `500 Server Error`. This is often good, because you won't be silencing errors, that you need to pay attention to.\n\n**Now, there are some quirks, that we need to pay attention to.**\n\n#### DRF's `ValidationError`\n\nFor example, if we simply raise a `rest_framework.exceptions.ValidationError` like that:\n\n```python\nfrom rest_framework import exceptions\n\n\ndef some_service():\n    raise ValidationError(\"Error message here.\")\n```\n\nThe response payload is going to look like this:\n\n```json\n[\"Some message\"]\n```\n\nThis looks strange, because if we do it like this:\n\n```python\nfrom rest_framework import exceptions\n\n\ndef some_service():\n    raise exceptions.ValidationError({\"error\": \"Some message\"})\n```\n\nThe response payload is going to look like this:\n\n```json\n{\n  \"error\": \"Some message\"\n}\n```\n\nThat's basically what we passed as the `detail` of the `ValidationError`. But it's a different data structure from the initial array.\n\nNow, if we decide to raise another of the DRF's built-in exceptions:\n\n```python\nfrom rest_framework import exceptions\n\n\ndef some_service():\n    raise exceptions.NotFound()\n```\n\nThe response payload is going to look like this:\n\n```json\n{\n  \"detail\": \"Not found.\"\n}\n```\n\nThat's entirely different from what we saw as behavior from the `ValidationError` and this might cause problems.\n\nSo far, the default DRF behavior can get us:\n\n- An array.\n- A dictionary.\n- A specific `{\"detail\": \"something\"}` result.\n\n**So if we need to use the default DRF behavior, we need to take care of this inconsistency.**\n\n#### Django's `ValidationError`\n\nNow, DRF's default exception handling is not playing nice with Django's `ValidationError`.\n\nThis piece of code:\n\n```python\nfrom django.core.exceptions import ValidationError as DjangoValidationError\n\n\ndef some_service():\n    raise DjangoValidationError(\"Some error message\")\n```\n\nWill result in an unhandled exception, causing `500 Server Error`.\n\nThis will also happen if this `ValidationError` comes from model validation, for example:\n\n```python\ndef some_service():\n    user = BaseUser()\n    user.full_clean()  # Throws ValidationError\n    user.save()\n```\n\nThis will also result in `500 Server Error`.\n\nIf we want to start handling this, as if it was `rest_framework.exceptions.ValidationError`, we need to roll-out our own [custom exception handler](https://www.django-rest-framework.org/api-guide/exceptions/#custom-exception-handling):\n\n```python\nfrom django.core.exceptions import ValidationError as DjangoValidationError\n\nfrom rest_framework.views import exception_handler\nfrom rest_framework.serializers import as_serializer_error\nfrom rest_framework import exceptions\n\n\ndef custom_exception_handler(exc, ctx):\n    if isinstance(exc, DjangoValidationError):\n        exc = exceptions.ValidationError(as_serializer_error(exc))\n\n    response = exception_handler(exc, ctx)\n\n    # If unexpected error occurs (server error, etc.)\n    if response is None:\n        return response\n\n    return response\n```\n\nThis is basically the default implementation, with the addition of this piece of code:\n\n```python\nif isinstance(exc, DjangoValidationError):\n    exc = exceptions.ValidationError(as_serializer_error(exc))\n```\n\nSince we need to map between `django.core.exceptions.ValidationError` and `rest_framework.exceptions.ValidationError`, we are using DRF's `as_serializer_error`, which is used internally in the serializers, just for that.\n\nWith that, we can now have Django's `ValidationError` playing nice with DRF's exception handler.\n\n### Describe how your API errors are going to look like.\n\nThis is very important and should be done as early as possible in any given project.\n\nThis is basically agreeing upon what the interface of your API errors - **How an error is going to look like as an API response?**\n\nThis is very project specific, you can use some of the popular APIs for inspiration:\n\n- Stripe - \u003chttps://stripe.com/docs/api/errors\u003e\n\nAs an example, we might decide that our errors are going to look like this:\n\n1. `4**` and `5**` status codes for different types of errors.\n1. Each error will be a dictionary with a single `message` key, containing the error message.\n\n```json\n{\n  \"message\": \"Some error message here\"\n}\n```\n\nThat's simple enough:\n\n- `400` will be used for validation errors.\n- `401` for auth errors.\n- `403` for permission errors.\n- `404` for not found errors.\n- `429` for throttling errors.\n- `500` for server errors (we need to be careful not to silence an exception causing 500 and always report that in services like Sentry)\n\nAgain, this is up to you \u0026 it's specific to the project. **We'll propose something similiar for one of the specific approaches.**\n\n### Know how to change the default exception handling behavior.\n\nThis is also important, because when you decide how your errors are going to look like, you need to implement this as custom exception handling.\n\nWe've already provided an example for that in the paragraph above, talking about Django's `ValidationError`.\n\nWe'll also provide additional examples in the sections below.\n\n### Approach 1 - Use DRF's default exceptions, with very little modifications.\n\nDRF's error handling is good. It'd be great, if the end result was always consistent. Those are the little modifications that we are going to do.\n\nWe want to end up with errors, always looking like that:\n\n```json\n{\n  \"detail\": \"Some error\"\n}\n```\n\nor\n\n```json\n{\n  \"detail\": [\"Some error\", \"Another error\"]\n}\n```\n\nor\n\n```json\n{\n  \"detail\": { \"key\": \"... some arbitrary nested structure ...\" }\n}\n```\n\nBasically, make sure we always have a dictionary with a `detail` key.\n\nAdditonally, we want to handle Django's `ValidationError` as well.\n\nIn order to achieve that, this is how our custom exception handler is going to look like:\n\n```python\nfrom django.core.exceptions import ValidationError as DjangoValidationError, PermissionDenied\nfrom django.http import Http404\n\nfrom rest_framework.views import exception_handler\nfrom rest_framework import exceptions\nfrom rest_framework.serializers import as_serializer_error\n\n\ndef drf_default_with_modifications_exception_handler(exc, ctx):\n    if isinstance(exc, DjangoValidationError):\n        exc = exceptions.ValidationError(as_serializer_error(exc))\n\n    if isinstance(exc, Http404):\n        exc = exceptions.NotFound()\n\n    if isinstance(exc, PermissionDenied):\n        exc = exceptions.PermissionDenied()\n\n    response = exception_handler(exc, ctx)\n\n    # If unexpected error occurs (server error, etc.)\n    if response is None:\n        return response\n\n    if isinstance(exc.detail, (list, dict)):\n        response.data = {\n            \"detail\": response.data\n        }\n\n    return response\n```\n\nWe kind-of replicate the original exception handler, so we can deal with an `APIException` after that (looking for `detail`).\n\nNow, lets run a set of tests:\n\nCode:\n\n```python\ndef some_service():\n    raise DjangoValidationError(\"Some error message\")\n```\n\nResponse:\n\n```json\n{\n  \"detail\": {\n    \"non_field_errors\": [\"Some error message\"]\n  }\n}\n```\n\n---\n\nCode:\n\n```python\nfrom django.core.exceptions import PermissionDenied\n\ndef some_service():\n    raise PermissionDenied()\n```\n\nResponse:\n\n```json\n{\n  \"detail\": \"You do not have permission to perform this action.\"\n}\n```\n\n---\n\nCode:\n\n```python\nfrom django.http import Http404\n\ndef some_service():\n    raise Http404()\n```\n\nResponse:\n\n```json\n{\n  \"detail\": \"Not found.\"\n}\n```\n\n---\n\nCode:\n\n```python\ndef some_service():\n    raise RestValidationError(\"Some error message\")\n```\n\nResponse:\n\n```json\n{\n  \"detail\": [\"Some error message\"]\n}\n```\n\n---\n\nCode:\n\n```python\ndef some_service():\n    raise RestValidationError(detail={\"error\": \"Some error message\"})\n```\n\nResponse:\n\n```json\n{\n  \"detail\": {\n    \"error\": \"Some error message\"\n  }\n}\n```\n\n---\n\nCode:\n\n```python\nclass NestedSerializer(serializers.Serializer):\n    bar = serializers.CharField()\n\n\nclass PlainSerializer(serializers.Serializer):\n    foo = serializers.CharField()\n    email = serializers.EmailField(min_length=200)\n\n    nested = NestedSerializer()\n\n\ndef some_service():\n    serializer = PlainSerializer(data={\n        \"email\": \"foo\",\n        \"nested\": {}\n    })\n    serializer.is_valid(raise_exception=True)\n\n```\n\nResponse:\n\n```json\n{\n  \"detail\": {\n    \"foo\": [\"This field is required.\"],\n    \"email\": [\n      \"Ensure this field has at least 200 characters.\",\n      \"Enter a valid email address.\"\n    ],\n    \"nested\": {\n      \"bar\": [\"This field is required.\"]\n    }\n  }\n}\n```\n\n---\n\nCode:\n\n```python\nfrom rest_framework import exceptions\n\n\ndef some_service():\n    raise exceptions.Throttled()\n```\n\nResponse:\n\n```json\n{\n  \"detail\": \"Request was throttled.\"\n}\n```\n\n---\n\nCode:\n\n```python\ndef some_service():\n    user = BaseUser()\n    user.full_clean()\n```\n\nResponse:\n\n```json\n{\n  \"detail\": {\n    \"password\": [\"This field cannot be blank.\"],\n    \"email\": [\"This field cannot be blank.\"]\n  }\n}\n```\n\n### Approach 2 - HackSoft's proposed way\n\nWe are going to propose an approach, that can be easily extended into something that works well for you.\n\n**Here are the key ideas:**\n\n1. **Your application will have its own hierarchy of exceptions**, that are going to be thrown by the business logic.\n1. Lets say, for simplicity, that we are going to have only 1 error - `ApplicationError`.\n   - This is going to be defined in a special `core` app, within `exceptions` module. Basically, having `project.core.exceptions.ApplicationError`.\n1. We want to let DRF handle everything else, by default.\n1. `ValidationError` is now special and it's going to be handled differently.\n   - `ValidationError` should only come from either serializer or a model validation.\n\n---\n\n**We are going to define the following structure for our errors:**\n\n```json\n{\n  \"message\": \"The error message here\",\n  \"extra\": {}\n}\n```\n\nThe `extra` key can hold arbitrary data, for the purposes of passing information to the frontend.\n\nFor example, whenever we have a `ValidationError` (usually coming from a Serializer or a Model), we are going to present the error like that:\n\n```json\n{\n  \"message\": \"Validation error.\",\n  \"extra\": {\n    \"fields\": {\n      \"password\": [\"This field cannot be blank.\"],\n      \"email\": [\"This field cannot be blank.\"]\n    }\n  }\n}\n```\n\nThis can be communicated with the frontend, so they can look for `extra.fields`, to present those specific errors to the user.\n\nIn order to achieve that, the custom exception handler is going to look like this:\n\n```python\nfrom django.core.exceptions import ValidationError as DjangoValidationError, PermissionDenied\nfrom django.http import Http404\n\nfrom rest_framework.views import exception_handler\nfrom rest_framework import exceptions\nfrom rest_framework.serializers import as_serializer_error\nfrom rest_framework.response import Response\n\nfrom styleguide_example.core.exceptions import ApplicationError\n\n\ndef hacksoft_proposed_exception_handler(exc, ctx):\n    \"\"\"\n    {\n        \"message\": \"Error message\",\n        \"extra\": {}\n    }\n    \"\"\"\n    if isinstance(exc, DjangoValidationError):\n        exc = exceptions.ValidationError(as_serializer_error(exc))\n\n    if isinstance(exc, Http404):\n        exc = exceptions.NotFound()\n\n    if isinstance(exc, PermissionDenied):\n        exc = exceptions.PermissionDenied()\n\n    response = exception_handler(exc, ctx)\n\n    # If unexpected error occurs (server error, etc.)\n    if response is None:\n        if isinstance(exc, ApplicationError):\n            data = {\n                \"message\": exc.message,\n                \"extra\": exc.extra\n            }\n            return Response(data, status=400)\n\n        return response\n\n    if isinstance(exc.detail, (list, dict)):\n        response.data = {\n            \"detail\": response.data\n        }\n\n    if isinstance(exc, exceptions.ValidationError):\n        response.data[\"message\"] = \"Validation error\"\n        response.data[\"extra\"] = {\n            \"fields\": response.data[\"detail\"]\n        }\n    else:\n        response.data[\"message\"] = response.data[\"detail\"]\n        response.data[\"extra\"] = {}\n\n    del response.data[\"detail\"]\n\n    return response\n```\n\nTake a look at that code \u0026 try to understand what's going on. **The strategy is - reuse as much as possible from DRF \u0026 then adjust.**\n\nNow, we are going to have the following behavior:\n\nCode:\n\n```python\nfrom styleguide_example.core.exceptions import ApplicationError\n\n\ndef trigger_application_error():\n    raise ApplicationError(message=\"Something is not correct\", extra={\"type\": \"RANDOM\"})\n```\n\nResponse:\n\n```json\n{\n  \"message\": \"Something is not correct\",\n  \"extra\": {\n    \"type\": \"RANDOM\"\n  }\n}\n```\n\n---\n\nCode:\n\n```python\ndef some_service():\n    raise DjangoValidationError(\"Some error message\")\n```\n\nResponse:\n\n```json\n{\n  \"message\": \"Validation error\",\n  \"extra\": {\n    \"fields\": {\n      \"non_field_errors\": [\"Some error message\"]\n    }\n  }\n}\n```\n\n---\n\nCode:\n\n```python\nfrom django.core.exceptions import PermissionDenied\n\ndef some_service():\n    raise PermissionDenied()\n```\n\nResponse:\n\n```json\n{\n  \"message\": \"You do not have permission to perform this action.\",\n  \"extra\": {}\n}\n```\n\n---\n\nCode:\n\n```python\nfrom django.http import Http404\n\ndef some_service():\n    raise Http404()\n```\n\nResponse:\n\n```json\n{\n  \"message\": \"Not found.\",\n  \"extra\": {}\n}\n```\n\n---\n\nCode:\n\n```python\ndef some_service():\n    raise RestValidationError(\"Some error message\")\n```\n\nResponse:\n\n```json\n{\n  \"message\": \"Validation error\",\n  \"extra\": {\n    \"fields\": [\"Some error message\"]\n  }\n}\n```\n\n---\n\nCode:\n\n```python\ndef some_service():\n    raise RestValidationError(detail={\"error\": \"Some error message\"})\n```\n\nResponse:\n\n```json\n{\n  \"message\": \"Validation error\",\n  \"extra\": {\n    \"fields\": {\n      \"error\": \"Some error message\"\n    }\n  }\n}\n```\n\n---\n\nCode:\n\n```python\nclass NestedSerializer(serializers.Serializer):\n    bar = serializers.CharField()\n\n\nclass PlainSerializer(serializers.Serializer):\n    foo = serializers.CharField()\n    email = serializers.EmailField(min_length=200)\n\n    nested = NestedSerializer()\n\n\ndef some_service():\n    serializer = PlainSerializer(data={\n        \"email\": \"foo\",\n        \"nested\": {}\n    })\n    serializer.is_valid(raise_exception=True)\n\n```\n\nResponse:\n\n```json\n{\n  \"message\": \"Validation error\",\n  \"extra\": {\n    \"fields\": {\n      \"foo\": [\"This field is required.\"],\n      \"email\": [\n        \"Ensure this field has at least 200 characters.\",\n        \"Enter a valid email address.\"\n      ],\n      \"nested\": {\n        \"bar\": [\"This field is required.\"]\n      }\n    }\n  }\n}\n```\n\n---\n\nCode:\n\n```python\nfrom rest_framework import exceptions\n\n\ndef some_service():\n    raise exceptions.Throttled()\n```\n\nResponse:\n\n```json\n{\n  \"message\": \"Request was throttled.\",\n  \"extra\": {}\n}\n```\n\n---\n\nCode:\n\n```python\ndef some_service():\n    user = BaseUser()\n    user.full_clean()\n```\n\nResponse:\n\n```json\n{\n  \"message\": \"Validation error\",\n  \"extra\": {\n    \"fields\": {\n      \"password\": [\"This field cannot be blank.\"],\n      \"email\": [\"This field cannot be blank.\"]\n    }\n  }\n}\n```\n\n---\n\nNow, this can be extended \u0026 made to better suit your needs:\n\n1. You can have `ApplicationValidationError` and `ApplicationPermissionError`, as an additional hierarchy.\n1. You can reimplement DRF's default exception handler, instead of reusing it (copy-paste it \u0026 adjust to your needs).\n\n**The general idea is - figure out what kind of error handling you need and then implement it accordingly.**\n\n### More ideas\n\nAs you can see, we can mold exception handling to our needs.\n\nYou can start handling more stuff - for example - translating `django.core.exceptions.ObjectDoesNotExist` to `rest_framework.exceptions.NotFound`.\n\nYou can even handle all exceptions, but then, you should be sure those exceptions are being logged properly, otherwise you might silence something that's important.\n\n## Testing\n\n### Overview\n\nTesting is an interesting \u0026 vast topic.\n\nAs an overview, you can listen to [Radoslav Georgiev's talk at DjangoCon Europe 2022](https://www.youtube.com/watch?v=PChaEAIsQls):\n\n[![Quality Assurance in Django - Testing what matters](https://img.youtube.com/vi/PChaEAIsQls/0.jpg)](https://www.youtube.com/watch?v=PChaEAIsQls)\n\nIn our Django projects, we split our tests depending on the type of code they represent.\n\nMeaning, we generally have tests for models, services, selectors \u0026 APIs / views.\n\nThe file structure usually looks like this:\n\n```\nproject_name\n├── app_name\n│   ├── __init__.py\n│   └── tests\n│       ├── __init__.py\n│       ├── factories.py\n│       ├── models\n│       │   └── __init__.py\n│       │   └── test_some_model_name.py\n│       ├── selectors\n│       │   └── __init__.py\n│       │   └── test_some_selector_name.py\n│       └── services\n│           ├── __init__.py\n│           └── test_some_service_name.py\n└── __init__.py\n```\n\n### Naming conventions\n\nWe follow 2 general naming conventions:\n\n- The test file names should be `test_the_name_of_the_thing_that_is_tested.py`\n- The test case should be `class TheNameOfTheThingThatIsTestedTests(TestCase):`\n\nFor example, if we have:\n\n```python\ndef a_very_neat_service(*args, **kwargs):\n    pass\n```\n\nWe are going to have the following for file name:\n\n```\nproject_name/app_name/tests/services/test_a_very_neat_service.py\n```\n\nAnd the following for test case:\n\n```python\nclass AVeryNeatServiceTests(TestCase):\n    pass\n```\n\nFor tests of utility functions, we follow a similar pattern.\n\nFor example, if we have `project_name/common/utils.py`, then we are going to have `project_name/common/tests/test_utils.py` and place different test cases in that file.\n\nIf we are to split the `utils.py` module into submodules, the same will happen for the tests:\n\n- `project_name/common/utils/files.py`\n- `project_name/common/tests/utils/test_files.py`\n\nWe try to match the structure of our modules with the structure of their respective tests.\n\n### Factories\n\nFactories are a great tool for generating data for your tests.\n\nWhen used correctly, you can improve the overall quality of your tests.\n\nIf you are new to this concept, you can refer to the following materials:\n\n- [Improve your Django tests with fakes and factories](https://www.hacksoft.io/blog/improve-your-tests-django-fakes-and-factories)\n- [https://www.hacksoft.io/blog/improve-your-tests-django-fakes-and-factories-advanced-usage](https://www.hacksoft.io/blog/improve-your-tests-django-fakes-and-factories-advanced-usage)\n- [DjangoCon 2022 | factory_boy: testing like a pro](https://www.youtube.com/watch?v=-C-XNHAJF-c)\n\n## Celery\n\nWe use [Celery](http://www.celeryproject.org/) for the following general cases:\n\n- Communicating with 3rd party services (sending emails, notifications, etc.)\n- Offloading heavier computational tasks outside the HTTP cycle.\n- Periodic tasks (using Celery beat)\n\n### The basics\n\nWe try to treat Celery as if it's just another interface to our core logic - meaning - **don't put business logic there.**\n\nLets look at an example of a **service** that sends emails (example taken from [`Django-Styleguide-Example`](https://github.com/HackSoftware/Django-Styleguide-Example/blob/master/styleguide_example/emails/tasks.py))\n\n```python\nfrom django.db import transaction\nfrom django.core.mail import EmailMultiAlternatives\n\nfrom styleguide_example.core.exceptions import ApplicationError\nfrom styleguide_example.common.services import model_update\nfrom styleguide_example.emails.models import Email\n\n\n@transaction.atomic\ndef email_send(email: Email) -\u003e Email:\n    if email.status != Email.Status.SENDING:\n        raise ApplicationError(f\"Cannot send non-ready emails. Current status is {email.status}\")\n\n    subject = email.subject\n    from_email = \"styleguide-example@hacksoft.io\"\n    to = email.to\n\n    html = email.html\n    plain_text = email.plain_text\n\n    msg = EmailMultiAlternatives(subject, plain_text, from_email, [to])\n    msg.attach_alternative(html, \"text/html\")\n\n    msg.send()\n\n    email, _ = model_update(\n        instance=email,\n        fields=[\"status\", \"sent_at\"],\n        data={\n            \"status\": Email.Status.SENT,\n            \"sent_at\": timezone.now()\n        }\n    )\n    return email\n```\n\nEmail sending has business logic around it, **but we still want to trigger this particular service from a task.**\n\nOur task looks like that:\n\n```python\nfrom celery import shared_task\n\nfrom styleguide_example.emails.models import Email\n\n\n@shared_task\ndef email_send(email_id):\n    email = Email.objects.get(id=email_id)\n\n    from styleguide_example.emails.services import email_send\n    email_send(email)\n```\n\nAs you can see, **we treat the task as an API:**\n\n1. Fetch the required data.\n2. Call the appropriate service.\n\nNow, imagine we have a different service, that triggers the email sending.\n\nIt may look like that:\n\n```python\nfrom django.db import transaction\n\n# ... more imports here ...\n\nfrom styleguide_example.emails.tasks import email_send as email_send_task\n\n\n@transaction.atomic\ndef user_complete_onboarding(user: User) -\u003e User:\n    # ... some code here\n\n    email = email_get_onboarding_template(user=user)\n\n    transaction.on_commit(lambda: email_send_task.delay(email.id))\n\n    return user\n```\n\n2 important things to point out here:\n\n1. We are importing the task (which has the same name as the service), but we are giving it a `_task` suffix.\n1. And when the transaction commits, we'll call the task.\n\n**So, in general, the way we use Celery can be described as:**\n\n1. Tasks call services.\n2. We import the service in the function body of the task.\n3. When we want to trigger a task, we import the task, at module level, giving the `_task` suffix.\n4. We execute tasks, as a side effect, whenever our transaction commits.\n\nThis way of mixing tasks \u0026 services also **prevents circular imports**, which may occurr often enough when using Celery.\n\n### Error handling\n\nSometimes, our service can fail and we might want to handle the error on the task level. For example - we might want to retry the task.\n\nThis error handling code needs to live in the task.\n\nLets expand the `email_send` task example from above, by adding error handling:\n\n```python\nfrom celery import shared_task\nfrom celery.utils.log import get_task_logger\n\nfrom styleguide_example.emails.models import Email\n\n\nlogger = get_task_logger(__name__)\n\n\ndef _email_send_failure(self, exc, task_id, args, kwargs, einfo):\n    email_id = args[0]\n    email = Email.objects.get(id=email_id)\n\n    from styleguide_example.emails.services import email_failed\n\n    email_failed(email)\n\n\n@shared_task(bind=True, on_failure=_email_send_failure)\ndef email_send(self, email_id):\n    email = Email.objects.get(id=email_id)\n\n    from styleguide_example.emails.services import email_send\n\n    try:\n        email_send(email)\n    except Exception as exc:\n        # https://docs.celeryq.dev/en/stable/userguide/tasks.html#retrying\n        logger.warning(f\"Exception occurred while sending email: {exc}\")\n        self.retry(exc=exc, countdown=5)\n```\n\nAs you can see, we do a bunch of retries and if all of them fail, we handle this in the `on_failure` callback.\n\nThe callback follows the naming pattern of `_{task_name}_failure` and it calls the service layer, just like an ordinary task.\n\n### Configuration\n\nWe pretty much follow the official guidelines of integrating Celery with Django - \u003chttps://docs.celeryq.dev/en/stable/django/first-steps-with-django.html\u003e\n\nFor a full example, you can check the Celery configuration in the `Django-Styleguide-Example` project:\n\n- \u003chttps://github.com/HackSoftware/Django-Styleguide-Example/tree/master/styleguide_example/tasks\u003e\n- \u003chttps://github.com/HackSoftware/Django-Styleguide-Example/blob/master/styleguide_example/tasks/celery.py\u003e\n\nCelery is a complex topic, so it's a good idea to invest time reading the documentation \u0026 understanding the different configuration options.\n\nWe constantly do that \u0026 find new things or find better approaches to our problems.\n\n### Structure\n\nTasks are located in `tasks.py` modules in different apps.\n\nWe follow the same rules as with everything else (APIs, services, selectors): **if the tasks for a given app grow too big, split them by domain.**\n\nMeaning, you can end up with `tasks/domain_a.py` and `tasks/domain_b.py`. All you need to do is import them in `tasks/__init__.py` for Celery to autodiscover them.\n\nThe general rule of thumb is - split your tasks in a way that'll make sense to you.\n\n### Periodic Tasks\n\nManaging periodic tasks is quite important, especially when you have tens or hundreds of them.\n\nWe use [Celery Beat](https://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html) + `django_celery_beat.schedulers:DatabaseScheduler` + [`django-celery-beat`](https://github.com/celery/django-celery-beat) for our periodic tasks.\n\nThe extra thing that we do is to have a management command, called [`setup_periodic_tasks`](https://github.com/HackSoftware/Django-Styleguide-Example/blob/master/styleguide_example/tasks/management/commands/setup_periodic_tasks.py), which holds the definition of all periodic tasks within the system. This command is located in the `tasks` app, discussed above.\n\nHere's how `project.tasks.management.commands.setup_periodic_tasks.py` looks like:\n\n```python\nfrom django.core.management.base import BaseCommand\nfrom django.db import transaction\n\nfrom django_celery_beat.models import IntervalSchedule, CrontabSchedule, PeriodicTask\n\nfrom project.app.tasks import some_periodic_task\n\n\nclass Command(BaseCommand):\n    help = f\"\"\"\n    Setup celery beat periodic tasks.\n\n    Following tasks will be created:\n\n    - {some_periodic_task.name}\n    \"\"\"\n\n    @transaction.atomic\n    def handle(self, *args, **kwargs):\n        print('Deleting all periodic tasks and schedules...\\n')\n\n        IntervalSchedule.objects.all().delete()\n        CrontabSchedule.objects.all().delete()\n        PeriodicTask.objects.all().delete()\n\n        periodic_tasks_data = [\n            {\n                'task': some_periodic_task\n                'name': 'Do some peridoic stuff',\n                # https://crontab.guru/#15_*_*_*_*\n                'cron': {\n                    'minute': '15',\n                    'hour': '*',\n                    'day_of_week': '*',\n                    'day_of_month': '*',\n                    'month_of_year': '*',\n                },\n                'enabled': True\n            },\n        ]\n\n        for periodic_task in periodic_tasks_data:\n            print(f'Setting up {periodic_task[\"task\"].name}')\n\n            cron = CrontabSchedule.objects.create(\n                **periodic_task['cron']\n            )\n\n            PeriodicTask.objects.create(\n                name=periodic_task['name'],\n                task=periodic_task['task'].name,\n                crontab=cron,\n                enabled=periodic_task['enabled']\n            )\n```\n\nFew key things:\n\n- We use this task as part of a deploy procedure.\n- We always put a link to [`crontab.guru`](https://crontab.guru) to explain the cron. Otherwise it's unreadable.\n- Everything is in one place.\n- ⚠️ We use, almost exclusively, a cron schedule. **If you plan on using the other schedule objects, provided by Celery, please read thru their documentation** \u0026 the important notes - \u003chttps://django-celery-beat.readthedocs.io/en/latest/#example-creating-interval-based-periodic-task\u003e - about pointing to the same schedule object. ⚠️\n\n### Beyond\n\nCelery has powerful tools to implement complex workflows - \u003chttps://docs.celeryq.dev/en/stable/userguide/canvas.html\u003e\n\nIf you decide to use them, the rules still apply.\n\nYou may need to reorganize things a bit, but as long as you have a well-defined interface to your application core, you'll be able to mix and match tasks \u0026 services in more complex scenarios.\n\n**More complex scenarios depend on their context. Make sure you are aware of the architecture \u0026 the decisions you are making.**\n\n## Cookbook\n\nSome of the implementations of generic reusable pieces of code are stored here.\n\n### Handling updates with a service\n\nAs for updating, we have a generic update service that we use inside of the actual update services. Here's what a sample `user_update` service would look like:\n\n```python\ndef user_update(*, user: User, data) -\u003e User:\n    non_side_effect_fields = ['first_name', 'last_name']\n\n    user, has_updated = model_update(\n        instance=user,\n        fields=non_side_effect_fields,\n        data=data\n    )\n\n    # Side-effect fields update here (e.g. username is generated based on first \u0026 last name)\n\n    # ... some additional tasks with the user ...\n\n    return user\n```\n\n- We're calling the generic `model_update` service for the fields that have no side-effects related to them (meaning that they're just set to the value that we provide).\n- This pattern allows us to extract the repetitive field setting in a generic service and perform only the specific tasks inside of the update service (side-effects).\n- We can be smart \u0026 provide the `update_fields` kwarg, when saving the instance. This way, in the `UPDATE` query, we'll only send values that are actually updated.\n\nThe full implementations of these services can be found in our example project:\n\n- [`model_update`](https://github.com/HackSoftware/Django-Styleguide-Example/blob/master/styleguide_example/common/services.py)\n- [`user_update`](https://github.com/HackSoftware/Django-Styleguide-Example/blob/master/styleguide_example/users/services.py)\n\nIf you are going to include `model_update` in your project, make sure to [read the tests](https://github.com/HackSoftware/Django-Styleguide-Example/blob/master/styleguide_example/common/tests/services/test_model_update.py) \u0026 include them too!\n\n## DX (Developer Experience)\n\nA section with various things that can make your Django developer life better.\n\n### `mypy` / type annotations\n\nWhen it comes to [`mypy`](https://mypy.readthedocs.io/en/stable/index.html), we have the following philosophy:\n\n\u003e Use it, if it makes sense for you \u0026 helps you produce better software.\n\nIn HackSoft, we have:\n\n- Projects where we enforce `mypy` and are very strict about it.\n- Projects where types are more loose and `mypy` is not used at all.\n\nContext is king here.\n\nIn the [`Django-Styleguide-Example`](https://github.com/HackSoftware/Django-Styleguide-Example), we've configured `mypy`, using both \u003chttps://github.com/typeddjango/django-stubs\u003e and \u003chttps://github.com/typeddjango/djangorestframework-stubs/\u003e. You can check it as an example.\n\nAdditionally, this particular project - \u003chttps://github.com/wemake-services/wemake-django-template\u003e - also has `mypy` configuration.\n\nFigure out what is going to work best for you.\n\n## Django Styleguide in the Wild\n\nHere's a collection of different folks \u0026 companies, that have found the styleguide useful:\n\n---\n\n**Michael Valencia, CTO at [Facturedo](https://facturedo.com/)**\n\n\u003e The source code of our core project in Facturedo started to get messy.\n\u003e Business logic could be found in many, incoherent places. We needed a solution to structure our Django project and we found it in the Django Styleguide.\n\u003e\n\u003e We recommend it to anyone wanting to structure a medium to large-sized project.\n\u003e It's a well defined guide that's constantly evolving.\n\n---\n\n## Additional resources / Alternatives\n\nAdditional resources \u0026 other alternatives that we found useful and that can add value to the styleguide.\n\n- [Dan Palmer - Scaling Django to 500 apps (DjangoCon US 2021)](https://www.youtube.com/watch?v=NsHo-kThlqI)\n- [Django API Domains](https://phalt.github.io/django-api-domains/)\n- [A YC News discussion around the Django Styleguide](https://news.ycombinator.com/item?id=34337667) - you can potentially find additional useful things here.\n\n## Inspiration\n\nThe way we do Django is inspired by the following things:\n\n- The general idea for **separation of concerns**\n- [Boundaries by Gary Bernhardt](https://www.youtube.com/watch?v=yTkzNHF6rMs)\n- Rails service objects\n- Recently, I saw the [Cognitive Load is what matters](https://github.com/zakirullin/cognitive-load) article and it resonated with me. Some of the things mentioned there are also a key concept of the Django Styleguide.\n","funding_links":[],"categories":["Resources","Python","Uncategorized"],"sub_categories":["Educational","Uncategorized"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FHackSoftware%2FDjango-Styleguide","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FHackSoftware%2FDjango-Styleguide","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FHackSoftware%2FDjango-Styleguide/lists"}