Build an Employee Directory CRUD API
Build a tested Django REST Framework API for employee and department CRUD.
Introduction
30 Second Summary
Employee details often end up scattered across spreadsheets or documents. Finding the right person becomes harder as teams grow.
In this project, you will build a local employee directory CRUD API with Django REST Framework. The finished service lets anonymous visitors read records while authenticated users create, update, or delete them.
What You'll Build
You will demo a local employee directory that returns departments and employees as JSON while protecting every write operation.
By the end of this project, you'll have:
- A visible request-to-response flow where /api/ping/ confirms the request method as GET in JSON.
- A working employee directory where /api/ links to department and employee collections. Each resource supports collection requests plus detail requests.
- An eight-test API contract that proves every CRUD method. It also catches invalid data plus anonymous write attempts.
- Secret Mission: Filter the employee collection with ?department=<id> and protect the behavior with a ninth regression test.
Are there any prerequisites?
You need strong Python fundamentals plus a Linux machine with Python 3.12+, venv, and Visual Studio Code. No previous HTTP, Django, or backend experience is required.
Before We Start
Before any code, this is your moment to commit to an employee directory API built around Department and Employee resources. Building it without AI code generation develops the request, data, permission, and debugging foundations needed for your AIOps roadmap.
Set Up the Django Project
The employee directory needs a stable foundation before its request flow can be trusted. Package conflicts could make the same project behave differently across machines.
A Python virtual environment isolates this project's packages. Pinned versions of Django plus Django REST Framework make every later command reproducible.
In this step, get ready to:
- Create an isolated project workspace in Visual Studio Code.
- Install the pinned framework dependencies.
- Scaffold the project before configuring its API settings.
Prepare the isolated workspace
The .venv folder keeps this project's Python packages separate from packages installed elsewhere. Activating it makes the terminal use that isolated environment.
- Use your Linux application launcher to open Visual Studio Code.
- Use the folder picker to create a folder named employee-management-api.
- Select the new employee-management-api folder to open it in Visual Studio Code.
- Use the terminal panel to open an integrated terminal inside employee-management-api.
- Set up the isolated shell environment by running these commands:
python -m venv .venv
source .venv/bin/activate
What do these commands do?
- The first command creates the project-specific environment inside .venv.
- The second command activates that environment for the current terminal session.
- Confirm the .venv folder appears in the Visual Studio Code file sidebar.
- Confirm the terminal prompt indicates that .venv is active.
Virtual environment not active?
Confirm that the integrated terminal is inside the employee-management-api folder. The activation path only works from the folder containing .venv.
A shell configuration can also hide the active environment from the prompt. Help me verify that my project virtual environment is active.
The dependency file records the exact framework versions used by this project. Anyone can install the same environment from this file.
- Use the Visual Studio Code file sidebar to create requirements.txt inside employee-management-api.
- Add the pinned dependencies to requirements.txt by pasting this content:
Django==6.1.2
djangorestframework==3.18.3
What do these dependencies provide?
- Django 6.1.2 provides the project structure plus the database layer.
- Django REST Framework 3.18.3 provides the API request tools used in later steps.
- Save requirements.txt.
- Install the recorded dependencies into the active environment by running this command:
python -m pip install -r requirements.txt
What does this command do?
The command asks pip to read requirements.txt. Each pinned package is installed into the active .venv environment.
Good progress. Your isolated environment now contains the exact framework versions used throughout this project.
Dependencies not installing?
Confirm that requirements.txt is saved inside employee-management-api. Check that the active terminal is using .venv.
A network restriction can also interrupt package downloads. Help me troubleshoot this dependency installation.
✔️ Awesome, I've got everything!
Great. Double-check that you saved requirements.txt inside employee-management-api.
ⓧ I'd like to double check the full code
Django==6.1.2
djangorestframework==3.18.3
Scaffold the project structure
A Django project holds settings that apply to the whole backend. A Django app groups the employee directory's models plus its request behavior.
The scaffold commands create several framework files automatically. They complete without interactive questions.
- Generate the framework structure by running these commands:
django-admin startproject employee_api .
python manage.py startapp directory
What do these commands create?
- The first command creates manage.py plus the employee_api project package in the current folder.
- The second command creates the directory app that holds the employee directory code.
- Confirm manage.py appears in the Visual Studio Code file sidebar.
- Confirm the employee_api folder appears beside manage.py.
- Confirm the directory folder appears beside employee_api.
Scaffold files missing?
Confirm that the terminal remains inside employee-management-api. The first command needs the final dot because it places the project in the current folder.
Check that the dependency installation completed before running the scaffold commands. Help me diagnose the missing Django scaffold files.
Configure and verify Django
Django only loads apps listed in INSTALLED_APPS. Registering both packages makes the API framework plus the directory app available to the project.
- Select employee_api/settings.py from the Visual Studio Code file sidebar.
- Find the existing INSTALLED_APPS list.
- Replace that list with this configuration:
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"directory",
]
What does this configuration do?
- The built-in entries keep Django's generated project features enabled.
- The rest_framework entry activates Django REST Framework.
- The directory entry activates your employee directory app.
- Save employee_api/settings.py.
- Check the registered apps by running this command:
python manage.py check
What does this check prove?
Django loads the project settings plus every registered app. A successful check proves that both new entries can be imported.
You should see System check identified no issues in the terminal output.
Project check failing?
Confirm that both new app names remain inside the square brackets for INSTALLED_APPS. Check that each name has matching quotation marks.
Confirm that the directory folder sits beside manage.py. Help me troubleshoot this Django project check.
The API uses session authentication to identify signed-in requests. An explicit setting keeps the permission behavior deterministic in later tests.
- Scroll to the end of employee_api/settings.py.
- Add this REST Framework configuration below DEFAULT_AUTO_FIELD:
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.SessionAuthentication",
],
}
What does this setting control?
The authentication class tells Django REST Framework how requests prove their identity. This project uses Django's existing login session support.
- Save employee_api/settings.py.
Keep the generated secret key
Django generated a random value for SECRET_KEY when it created the project. The your-secret-key-here text in the comparison file represents that existing local value.
Your generated local value stays in employee_api/settings.py.
✔️ Awesome, I've got everything!
Great. Double-check that employee_api/settings.py is saved with your generated local SECRET_KEY value.
ⓧ I'd like to double check the full code
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = "your-secret-key-here"
DEBUG = True
ALLOWED_HOSTS = []
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"directory",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "employee_api.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "employee_api.wsgi.application"
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
STATIC_URL = "static/"
MAILERS = {
"default": {
"BACKEND": "django.core.mail.backends.console.EmailBackend",
},
}
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.SessionAuthentication",
],
}
Before you run the final checks, predict which version number appears first. Consider whether the new authentication setting changes the system check result.
- Verify the installed framework version plus the project configuration by running these commands:
python -m django --version
python manage.py check
What do these checks prove?
- The version command confirms that the active environment uses Django 6.1.2.
- The project check confirms that Django can load the scaffolded project plus its configured apps.
You should see 6.1.2 followed by System check identified no issues.
The development server runs continuously in the current terminal. You can stop it later with Ctrl-C.
Before you start the server, predict whether the completed configuration can begin listening for local requests.
- Start the local Django development server by running this command:
python manage.py runserver
What does this command do?
Django checks the project before starting its local development server. The terminal stays occupied while the server listens for requests.
- Confirm the terminal shows Django listening at http://127.0.0.1:8000/.
- Enter http://127.0.0.1:8000/ in your browser address bar.
- Load the local URL to confirm the Django page responds.
The browser connects to your local Django project. The terminal records the incoming browser request.
Local server not responding?
Confirm that the terminal running the server remains open. Check that it still shows the local address.
Confirm that the browser address includes the final slash after the port. Help me troubleshoot this local Django server.
That's the foundation in place. Next, you'll trace a real request from its URL to a JSON response.
Trace Your First HTTP Request
Your Django project can run locally. The next gap is tracing how one HTTP request reaches Python code.
A tiny endpoint makes the URL-to-view flow visible before viewsets handle that wiring. You will compare its JSON response with a request to the planned employee collection.
In this step, get ready to:
- Define a GET-only ping view that returns the incoming request method.
- Connect the ping view beneath the api/ URL prefix.
- Request the working ping path and the planned employee path.
Build the ping view
Django REST Framework can turn a Python function into an API view. Its decorator limits the accepted method before Response converts the returned data into an HTTP response.
- In the Visual Studio Code Explorer sidebar from earlier, select directory/views.py.
- Select all generated content in directory/views.py.
- Replace the selected content by pasting this code:
from rest_framework.decorators import api_view
from rest_framework.response import Response
@api_view(["GET"])
def ping(request):
return Response(
{
"message": "employee API is running",
"method": request.method,
}
)
What does this code do?
- The @api_view(["GET"]) decorator allows the view to receive GET requests.
- The ping(request) function receives the incoming request.
- The request.method value records the HTTP method used by the client.
- The Response object returns the message and request method as JSON.
- Save directory/views.py.
- Confirm the editor shows ping(request) beneath the two imports.
Seeing import warnings?
- Confirm the integrated terminal still uses the .venv environment from earlier.
- Check that both import lines begin with rest_framework.
Help me fix the imports in my ping view.
✔️ Awesome, I've got everything!
Great. Double-check that you saved directory/views.py.
ⓧ I'd like to double check the full code
from rest_framework.decorators import api_view
from rest_framework.response import Response
@api_view(["GET"])
def ping(request):
return Response(
{
"message": "employee API is running",
"method": request.method,
}
)
Connect the URL path
The view needs a named route before a client can reach it. The app URL configuration maps ping/ to the ping function.
- Select the directory folder in the Explorer sidebar.
- Use the Explorer toolbar to create a file inside directory.
- Name the new file urls.py.
- Add the ping route to directory/urls.py by pasting this code:
from django.urls import path
from .views import ping
urlpatterns = [
path("ping/", ping, name="ping"),
]
What does this route do?
- The path import provides Django's URL mapping function.
- The ping import connects the route to your new view.
- The name="ping" argument gives the route a reusable name.
- Save directory/urls.py.
- Confirm urls.py appears inside the directory folder in the Explorer sidebar.
Can't find the app URL file?
- Confirm urls.py sits beside views.py inside the directory folder.
- Check that the file name ends with .py.
Help me check the location of directory/urls.py.
✔️ Awesome, I've got everything!
Good progress. Your app now has a named route for the ping view.
ⓧ I'd like to double check the full code
from django.urls import path
from .views import ping
urlpatterns = [
path("ping/", ping, name="ping"),
]
The project URL configuration is the entry point for incoming paths. It delegates every request beneath api/ to directory/urls.py.
- Select employee_api/urls.py in the Explorer sidebar.
- Select all existing content in employee_api/urls.py.
- Replace the selected content by pasting this code:
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("api/", include("directory.urls")),
]
How do the URL files connect?
- The include import lets the project delegate part of its URL configuration to an app.
- The api/ prefix becomes the shared starting path for directory endpoints.
- The include("directory.urls") call loads the app route you just created.
The development server stays attached to its terminal while it listens for requests. A second terminal can send requests while this process keeps running.
- Save employee_api/urls.py.
- Start the development server in the existing terminal by running this command:
python manage.py runserver
What does this command do?
Django loads the project URL configuration and starts its local development server. The server listens for requests at http://127.0.0.1:8000/.
Your API route is now live locally. Keep this terminal running for the request checks.
Server not listening?
- Confirm the terminal is inside the employee-management-api folder.
- Check that employee_api/urls.py imports both include and path.
Help me diagnose why my Django development server is not starting.
✔️ Awesome, I've got everything!
Your root URL configuration is saved. The server is ready for a request.
ⓧ I'd like to double check the full code
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("api/", include("directory.urls")),
]
Trace both request outcomes
The ping route now gives you a complete request path to test. A Python client can call the URL and print the decoded response directly in the terminal.
- Create a second integrated terminal from the Terminal menu in Visual Studio Code.
Before you run this, predict which request method the response will report.
- Request the ping endpoint from the second terminal by running this command:
python -c 'from urllib.request import urlopen; print(urlopen("http://127.0.0.1:8000/api/ping/").read().decode())'
How does this request work?
- The urlopen function sends an HTTP request to the ping URL.
- The read() call retrieves the response body.
- The decode() call converts the response bytes into readable text.
You will see JSON containing "method":"GET". You have now traced a GET request from the URL to your Python view and back to the client.
Ping request not returning JSON?
- Confirm the first terminal still shows the running development server.
- Check that directory/urls.py maps ping/ to ping.
- Check that employee_api/urls.py includes directory.urls beneath api/.
Help me diagnose why the ping request does not return JSON.
The same client can now probe the planned employee collection. This check answers whether the current URL table can handle that request.
Before you run this, predict whether the planned employee request will return JSON or stop with an HTTP error.
- Request the planned employee endpoint from the second terminal by running this command:
python -c 'from urllib.request import urlopen; urlopen("http://127.0.0.1:8000/api/employees/")'
Why did this request fail?
- The client requested /api/employees/ from the running server.
- The current URL configuration has no employee route.
- Python surfaces the server response as HTTP Error 404.
That failure is intentional. It proves the request reached Django and exposed the exact routing gap your CRUD API must solve.
Your ping request now completes the full HTTP loop. Next, you will give the employee route a relational data model that can store useful records.
Model and Migrate the Employee Data
Your ping endpoint made the request path from a URL to Python code visible. The planned employee URL still ends in a 404 because no employee resource exists.
A Django model describes the data that belongs to each resource. A migration turns those definitions into a versioned SQLite schema that future CRUD routes can use.
In this step, get ready to:
- Define the employee directory's relational data model.
- Inspect the SQL for the initial migration.
- Apply the schema to SQLite.
Define the relational data model
A foreign key gives each Employee one Department. Database uniqueness blocks duplicate values in protected fields.
- Select directory/models.py in the Visual Studio Code Explorer sidebar.
- Replace the scaffolded contents with the Department model by pasting this code:
from django.db import models
class Department(models.Model):
name = models.CharField(max_length=100, unique=True)
def __str__(self):
return self.name
What does this model define?
- The models import provides Django's database field types.
- The Department class represents one department record.
- The name field accepts up to 100 characters.
- The unique=True rule prevents two departments from sharing a name.
- The __str__() method gives each department a readable label.
- Save directory/models.py.
- Confirm the editor tab no longer shows an unsaved marker.
Seeing errors in the Department model?
- Check that from django.db import models remains at the top of directory/models.py.
- Check that name is indented inside Department.
Help me diagnose errors in my Department model.
- Place your cursor below the Department __str__() method.
- Add the Employee model by pasting this code:
class Employee(models.Model):
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
email = models.EmailField(unique=True)
hired_on = models.DateField()
department = models.ForeignKey(
Department,
on_delete=models.CASCADE,
related_name="employees",
)
def __str__(self):
return f"{self.first_name} {self.last_name}"
How are employees connected?
- The name fields store each employee's first name plus last name.
- The email field rejects duplicate addresses.
- The hired_on field stores the employee's hiring date.
- The department field links each employee to one Department record.
- The models.CASCADE behavior removes linked employees when their department is deleted.
- The related_name="employees" value provides the reverse relationship from a department.
- Save directory/models.py.
- Confirm the file now contains both model classes.
Seeing errors in the Employee model?
- Check that Employee starts at the same indentation level as Department.
- Check that the three ForeignKey arguments remain inside matching parentheses.
- Check that Department appears before Employee in the file.
Help me diagnose errors in my Employee model.
✔️ Awesome, I've got everything!
Your saved model file now defines the two resources plus their relationship.
ⓧ I'd like to double check the full code
from django.db import models
class Department(models.Model):
name = models.CharField(max_length=100, unique=True)
def __str__(self):
return self.name
class Employee(models.Model):
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
email = models.EmailField(unique=True)
hired_on = models.DateField()
department = models.ForeignKey(
Department,
on_delete=models.CASCADE,
related_name="employees",
)
def __str__(self):
return f"{self.first_name} {self.last_name}"
Generate the migration plan
A migration file records the schema change before Django modifies the database. This gives you a versioned plan that you can inspect first.
- Switch back to the second integrated terminal from earlier.
- Generate the initial migration from your saved models by running this command:
python manage.py makemigrations
What does this command do?
- The makemigrations command compares your model definitions with the recorded migration history.
- Django writes the detected schema changes into the initial migration file.
- The migration contains operations for creating both model tables.
- Confirm directory/migrations/0001_initial.py appears in the Explorer sidebar.
The terminal lists a new initial migration for the directory app. It names one model-creation operation for each resource.
Migration file not created?
- Confirm the terminal is inside the employee-management-api folder.
- Confirm the terminal prompt still shows the active .venv environment.
- Save directory/models.py before rerunning the command.
Help me troubleshoot why Django is not creating the initial migration.
Before you inspect the plan, predict which model creates the foreign-key column.
- Render the initial migration as SQL by running this command:
python manage.py sqlmigrate directory 0001
What does this command reveal?
- The sqlmigrate command renders the selected migration as database SQL.
- The directory argument selects your app's migration history.
- The 0001 argument selects the initial migration.
- This inspection shows the planned schema before it is applied.
- Find the two CREATE TABLE statements in the terminal output.
- Locate the primary-key definition in each table.
- Locate the uniqueness rules for department names plus employee email addresses.
- Locate the department_id foreign-key column in the employee table.
The SQL shows one table for each model. It also shows the relationship plus the database-enforced constraints.
Missing a table or constraint?
- Confirm the command targets migration 0001 from the directory app.
- Check directory/models.py for both model classes.
- Check that unique=True remains on the department name plus employee email fields.
Help me inspect the generated SQL for the expected tables and constraints.
Apply the database schema
Applying the migration creates the schema in your local SQLite database. Django records the completed migration so it does not apply the same change twice.
- Apply the pending migrations to the local database by running this command:
python manage.py migrate
What happens during migration?
- The migrate command executes every pending migration in dependency order.
- The db.sqlite3 file stores the resulting local database schema.
- Django records the initial directory migration as applied.
- Confirm the terminal reports the initial directory migration as applied.
- Confirm db.sqlite3 appears in the employee-management-api folder.
Migration not applying?
- Confirm directory/migrations/0001_initial.py exists.
- Check the terminal output for the migration that stopped.
- Check directory/models.py for indentation or syntax problems.
Help me troubleshoot why the initial Django migration is not applying.
Before you check the migration history, predict how Django marks a migration that has already been applied.
- Display the directory app's migration state by running this command:
python manage.py showmigrations directory
What does this check prove?
- The showmigrations command lists migrations for the selected app.
- The marker beside each migration shows whether Django has applied it.
You'll see [X] 0001_initial. This confirms that the relational schema is active in db.sqlite3.
That's the data foundation in place. Next, you'll connect these models to validated JSON plus CRUD routes.
Turn the Models into a CRUD API
Your migrated tables now give the directory a relational home in SQLite. The /api/employees/ route still returns an HTTP 404 because no URL connects that data to CRUD behavior.
Django REST Framework serializers turn incoming JSON into validated model data. In this step, you connect both resources to their standard CRUD operations.
ModelViewSet supplies the actions behind each resource. DefaultRouter publishes those actions under consistent URLs.
In this step, get ready to:
- Define explicit serializer fields for both models.
- Protect each model viewset with permissions for anonymous reads.
- Publish router-generated routes that replace the planned 404.
Create explicit serializers
Each serializer is an allowlist for API data. Its fields control the keys used in responses. The same list controls accepted input.
- In the VS Code Explorer sidebar, select the directory folder.
- Click the New File icon in the Explorer toolbar.
- Enter serializers.py as the file name.
- Add both serializer definitions to directory/serializers.py by pasting this code:
from rest_framework import serializers
from .models import Department, Employee
class DepartmentSerializer(serializers.ModelSerializer):
class Meta:
model = Department
fields = ["id", "name"]
class EmployeeSerializer(serializers.ModelSerializer):
class Meta:
model = Employee
fields = [
"id",
"first_name",
"last_name",
"email",
"hired_on",
"department",
]
What do these serializers do?
- The DepartmentSerializer exposes the department identifier plus its name.
- The EmployeeSerializer exposes the six fields in its explicit allowlist.
- ModelSerializer derives field types from each model.
- The department field accepts a department primary key for employee input.
- Save directory/serializers.py.
- Confirm serializers.py appears inside the directory folder in the Explorer sidebar.
That is the data boundary in place. Both resources now have explicit API field lists.
Can't find the serializer file?
Check that serializers.py sits directly inside the directory folder beside models.py.
Compare every bracket if the editor highlights a syntax problem. Help me debug my directory/serializers.py file.
✔️ Awesome, I've got everything!
Both serializer classes are saved in directory/serializers.py.
ⓧ I'd like to double check the full code
from rest_framework import serializers
from .models import Department, Employee
class DepartmentSerializer(serializers.ModelSerializer):
class Meta:
model = Department
fields = ["id", "name"]
class EmployeeSerializer(serializers.ModelSerializer):
class Meta:
model = Employee
fields = [
"id",
"first_name",
"last_name",
"email",
"hired_on",
"department",
]
Add the model viewsets
A viewset binds HTTP methods to ORM work. Using one viewset per model keeps the resource behavior consistent.
- In the Explorer sidebar, select the existing directory/views.py file.
- Replace its contents with the cumulative code below:
from rest_framework import permissions, viewsets
from rest_framework.decorators import api_view
from rest_framework.response import Response
from .models import Department, Employee
from .serializers import DepartmentSerializer, EmployeeSerializer
@api_view(["GET"])
def ping(request):
return Response(
{
"message": "employee API is running",
"method": request.method,
}
)
class DepartmentViewSet(viewsets.ModelViewSet):
queryset = Department.objects.all()
serializer_class = DepartmentSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
class EmployeeViewSet(viewsets.ModelViewSet):
queryset = Employee.objects.all()
serializer_class = EmployeeSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
How is responsibility divided?
- The existing ping() function keeps the request-tracing endpoint available.
- Each queryset selects the model records available to its viewset.
- Each serializer_class controls response fields plus input validation.
- Each permission_classes setting uses IsAuthenticatedOrReadOnly to allow anonymous reads.
- Authenticated clients gain access to the write actions supplied by ModelViewSet.
- Save directory/views.py.
- Check the updated view imports through the project configuration by running this command:
python manage.py check
What does this check prove?
The check loads the configured Django project. That process imports the updated view module through the URL configuration.
You'll see System check identified no issues. That's the behavior layer wired in.
Does the project check fail?
Confirm that both serializer class names match their definitions in directory/serializers.py.
Check that every import remains at the top of directory/views.py. Help me diagnose this Django project check failure.
✔️ Awesome, I've got everything!
The updated directory/views.py loads without project check errors.
ⓧ I'd like to double check the full code
from rest_framework import permissions, viewsets
from rest_framework.decorators import api_view
from rest_framework.response import Response
from .models import Department, Employee
from .serializers import DepartmentSerializer, EmployeeSerializer
@api_view(["GET"])
def ping(request):
return Response(
{
"message": "employee API is running",
"method": request.method,
}
)
class DepartmentViewSet(viewsets.ModelViewSet):
queryset = Department.objects.all()
serializer_class = DepartmentSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
class EmployeeViewSet(viewsets.ModelViewSet):
queryset = Employee.objects.all()
serializer_class = EmployeeSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
Publish the router-generated routes
A router translates each viewset into collection routes plus detail routes. It also creates an API root that links to registered resources.
- In the Explorer sidebar, select the existing directory/urls.py file.
- Replace its contents with the router configuration below:
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from .views import DepartmentViewSet, EmployeeViewSet, ping
router = DefaultRouter()
router.register("departments", DepartmentViewSet)
router.register("employees", EmployeeViewSet)
urlpatterns = [
path("ping/", ping, name="ping"),
path("", include(router.urls)),
]
Which routes does the router create?
- Each router.register call connects a URL prefix to its model viewset.
- Collection GET requests map to list.
- Collection POST requests map to create.
- Detail GET requests map to retrieve.
- PUT maps to update. PATCH maps to partial update.
- DELETE maps to destroy.
- The explicit ping/ path keeps your earlier endpoint beside the router URLs.
- Save directory/urls.py.
✔️ Awesome, I've got everything!
The ping path plus both router registrations are saved in directory/urls.py.
ⓧ I'd like to double check the full code
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from .views import DepartmentViewSet, EmployeeViewSet, ping
router = DefaultRouter()
router.register("departments", DepartmentViewSet)
router.register("employees", EmployeeViewSet)
urlpatterns = [
path("ping/", ping, name="ping"),
path("", include(router.urls)),
]
- Return to the integrated terminal with the active .venv environment.
- Stop the earlier development server process if it is still active.
- Start the updated local server by running this command:
python manage.py runserver
What does this command do?
The command loads the updated URL configuration. The development server listens at http://127.0.0.1:8000/.
Does the server stop during startup?
Confirm that directory/urls.py imports both viewset names exactly as they appear in directory/views.py.
Check that the terminal still shows the active .venv environment. Help me troubleshoot this server startup failure.
- Keep the server process running in its terminal.
- Create a second terminal from VS Code's terminal panel.
Before you run the request, do you expect the old 404 or a router-generated collection response?
- Request the employee collection from the second terminal by running this command:
python -c 'from urllib.request import urlopen; print(urlopen("http://127.0.0.1:8000/api/employees/").read().decode())'
What does this request check?
The command sends a GET request to the employee collection. It prints the decoded response body in the terminal.
You'll see [] instead of the former 404. The router has connected the empty employee table to a working collection endpoint.
- Open the local API root in your browser.
You'll see links named departments plus employees. Your CRUD API now publishes both resources.
Still seeing the old 404?
Confirm that the running server restarted after you saved directory/urls.py.
Check that the employee registration uses the exact employees prefix. Help me diagnose why the router-generated employee endpoint still returns 404.
You have replaced the planned 404 with router-backed CRUD routes. Next, you will prove every method plus permission boundary with endpoint tests.
Prove Every Endpoint and Permission
Your router now exposes working collection routes and detail routes for both resources. The permission layer also separates public reads from authenticated changes.
A browsable API can still hide broken methods or authorization gaps. An APITestCase suite turns the complete HTTP contract into repeatable checks.
In this step, get ready to:
- Test the ping endpoint plus every department operation.
- Test every employee operation plus invalid data.
- Prove the anonymous-read and authenticated-write boundary.
Test ping and department behavior
Each test sends a request through the same URL and view layers used by a client. Django runs database tests in an isolated test database so your records in db.sqlite3 stay untouched.
- In the VS Code Explorer sidebar, select directory/tests.py.
- Select the generated contents of directory/tests.py.
- Start the suite with its imports plus the ping test by pasting this code:
from django.contrib.auth import get_user_model
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from .models import Department, Employee
class PingAPITests(APITestCase):
def test_ping_returns_request_method(self):
response = self.client.get(reverse("ping"))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["method"], "GET")
What Does This Test Prove?
- The imports provide URL reversal, status constants, the API test client, and both database models.
- The ping route name resolves the endpoint without hard-coding its URL.
- The assertions prove that the endpoint returns a successful response containing the incoming GET method.
- Save directory/tests.py.
- Run the first test through Django's test runner by running this command:
python manage.py test
What Does This Check Do?
Django discovers the test method in directory/tests.py. It runs the request against an isolated test environment.
The terminal reports a successful test run ending in OK. Your ping endpoint now has an executable contract.
Ping Test Not Passing?
Check that reverse("ping") matches the route name in directory/urls.py. Confirm that the response assertion uses the uppercase value GET.
Make sure the integrated terminal still has .venv activated. Help me diagnose why PingAPITests is failing.
Department tests need predictable records plus router-generated URLs. The shared setUp() method recreates that starting state before each test.
- Add the department fixture plus anonymous read test below PingAPITests by pasting this code:
class DepartmentAPITests(APITestCase):
def setUp(self):
self.user = get_user_model().objects.create_user(
username="backend-learner",
password="test-password",
)
self.department = Department.objects.create(name="Engineering")
self.list_url = reverse("department-list")
self.detail_url = reverse(
"department-detail",
args=[self.department.pk],
)
def test_anonymous_user_can_list_and_retrieve_departments(self):
list_response = self.client.get(self.list_url)
detail_response = self.client.get(self.detail_url)
self.assertEqual(list_response.status_code, status.HTTP_200_OK)
self.assertEqual(len(list_response.data), 1)
self.assertEqual(detail_response.status_code, status.HTTP_200_OK)
self.assertEqual(detail_response.data["name"], "Engineering")
How Does the Department Fixture Work?
- The fixture creates the test user used by the authenticated flow later.
- The Engineering record gives collection and detail requests a known result.
- The router names department-list and department-detail resolve the two route shapes.
- The read test proves that an anonymous client can list departments and retrieve one department.
- Save directory/tests.py.
- Check the expanded suite by running this command:
python manage.py test
What Does This Check Prove?
The test runner rebuilds the fixture for the new department test. A passing result proves both anonymous GET routes return the expected record.
The terminal ends in OK. Public department reads now have regression coverage.
Department Read Test Failing?
Confirm that the router registers departments with DepartmentViewSet. Check that the fixture creates exactly one department.
Verify that DepartmentSerializer includes id and name. Help me debug the department read test.
The next test attacks every write route without authentication. Each request must stop at the permission boundary before it changes a department.
- Add the anonymous department write test below the department read test by pasting this code:
def test_anonymous_user_cannot_write_departments(self):
responses = {
"create": self.client.post(
self.list_url,
{"name": "Finance"},
format="json",
),
"replace": self.client.put(
self.detail_url,
{"name": "Platform"},
format="json",
),
"partial_update": self.client.patch(
self.detail_url,
{"name": "Infrastructure"},
format="json",
),
"delete": self.client.delete(self.detail_url),
}
for operation, response in responses.items():
with self.subTest(operation=operation):
self.assertEqual(
response.status_code,
status.HTTP_403_FORBIDDEN,
)
What Does the Denial Test Cover?
- The dictionary sends anonymous POST, PUT, PATCH, and DELETE requests.
- Each subtest keeps the operation name attached to a possible failure.
- Every assertion expects HTTP_403_FORBIDDEN from the configured authentication and permission boundary.
- Save directory/tests.py.
- Verify all four anonymous write denials by running this command:
python manage.py test
What Does This Check Prove?
One test method exercises four denied department operations. The subtests verify that each write request receives the same permission response.
The suite ends in OK. Anonymous clients can read departments without gaining write access.
Anonymous Write Test Failing?
Confirm that DepartmentViewSet sets permission_classes to IsAuthenticatedOrReadOnly. Check that the test does not call force_authenticate before these requests.
Make sure SessionAuthentication remains the configured authentication class. Help me investigate the anonymous write status codes.
Authentication changes the same router routes into a full CRUD workflow. This test follows one department from creation through deletion.
- Add the authenticated create and replace flow below the anonymous write test by pasting this code:
def test_authenticated_user_can_create_update_and_delete_department(self):
self.client.force_authenticate(user=self.user)
create_response = self.client.post(
self.list_url,
{"name": "Finance"},
format="json",
)
self.assertEqual(create_response.status_code, status.HTTP_201_CREATED)
created_url = reverse(
"department-detail",
args=[create_response.data["id"]],
)
replace_response = self.client.put(
created_url,
{"name": "People Operations"},
format="json",
)
self.assertEqual(replace_response.status_code, status.HTTP_200_OK)
How Does the Authenticated Flow Begin?
- The test client authenticates as the user created by setUp().
- The POST request creates a department and captures its generated ID.
- The PUT request replaces the created department through its detail URL.
- Extend the authenticated method below its final assertion by pasting this code:
patch_response = self.client.patch(
created_url,
{"name": "Operations"},
format="json",
)
self.assertEqual(patch_response.status_code, status.HTTP_200_OK)
delete_response = self.client.delete(created_url)
self.assertEqual(delete_response.status_code, status.HTTP_204_NO_CONTENT)
self.assertFalse(
Department.objects.filter(pk=create_response.data["id"]).exists()
)
How Does the Authenticated Flow Finish?
- The PATCH request changes only the department name.
- The DELETE request expects the successful no-content status.
- The final ORM assertion proves that the deleted row no longer exists.
- Save directory/tests.py.
- Run the complete department flow by running this command:
python manage.py test
What Does This Check Prove?
The authenticated test exercises create, replace, partial update, and delete behavior. Its final query confirms that deletion changes the database state.
The suite ends in OK. Your department endpoints now have read, permission, and CRUD coverage.
Authenticated Department Flow Failing?
Confirm that self.user comes from get_user_model(). Check that force_authenticate runs before the first write request.
Verify that created_url uses the ID returned by the create response. Help me debug the authenticated department CRUD test.
Test employee behavior
Employee requests add a foreign key plus two validation boundaries. The fixture supplies a valid department and employee before every employee test.
- Add the employee test class plus its fixture below DepartmentAPITests by pasting this code:
class EmployeeAPITests(APITestCase):
def setUp(self):
self.user = get_user_model().objects.create_user(
username="backend-learner",
password="test-password",
)
self.department = Department.objects.create(name="Engineering")
self.employee = Employee.objects.create(
first_name="Ada",
last_name="Lovelace",
email="ada@example.com",
hired_on="2024-01-15",
department=self.department,
)
self.list_url = reverse("employee-list")
self.detail_url = reverse(
"employee-detail",
args=[self.employee.pk],
)
What Does the Employee Fixture Provide?
- The fixture creates the authenticated user needed by employee write tests.
- The department gives each employee a valid foreign key target.
- The Ada Lovelace record supplies known data for anonymous list and detail requests.
- The router names resolve the employee collection URL and the existing employee detail URL.
- Add the reusable employee payload below setUp() by pasting this code:
def employee_payload(self, **overrides):
payload = {
"first_name": "Grace",
"last_name": "Hopper",
"email": "grace@example.com",
"hired_on": "2024-02-01",
"department": self.department.pk,
}
payload.update(overrides)
return payload
Why Use a Payload Helper?
The helper keeps one valid employee request body in a single place. Individual tests can override one field to target a specific behavior.
- Save directory/tests.py.
- Check that the fixture and helper load cleanly by running this command:
python manage.py test
What Does This Check Prove?
Django imports the new test class and prepares its fixture definition. The passing suite confirms that the employee setup uses valid model fields and routes.
The existing tests still end in OK. The employee fixture is ready for endpoint checks.
Employee Fixture Not Loading?
Confirm that the employee uses the existing Department object. Check that hired_on uses the date format shown in the code.
Verify that the router names are employee-list and employee-detail. Help me debug the EmployeeAPITests fixture.
- Add the anonymous employee read test below employee_payload() by pasting this code:
def test_anonymous_user_can_list_and_retrieve_employees(self):
list_response = self.client.get(self.list_url)
detail_response = self.client.get(self.detail_url)
self.assertEqual(list_response.status_code, status.HTTP_200_OK)
self.assertEqual(len(list_response.data), 1)
self.assertEqual(detail_response.status_code, status.HTTP_200_OK)
self.assertEqual(detail_response.data["email"], "ada@example.com")
What Does the Employee Read Test Prove?
The collection request must return the one fixture employee. The detail request must return Ada's email without requiring authentication.
- Save directory/tests.py.
- Verify both employee read routes by running this command:
python manage.py test
What Does This Check Prove?
The new test sends anonymous requests to the employee collection and detail routes. Its assertions confirm the serializer returns the expected employee data.
The terminal ends in OK. Anonymous employee reads match the public API contract.
Employee Read Test Failing?
Confirm that EmployeeViewSet uses Employee.objects.all(). Check that EmployeeSerializer includes email.
Verify that the fixture creates only one employee. Help me diagnose the employee read response.
Employee writes use the same permission boundary as departments. Testing all four methods guards every router-generated mutation route.
- Add the anonymous employee write test below the employee read test by pasting this code:
def test_anonymous_user_cannot_write_employees(self):
responses = {
"create": self.client.post(
self.list_url,
self.employee_payload(),
format="json",
),
"replace": self.client.put(
self.detail_url,
self.employee_payload(email="ada@example.com"),
format="json",
),
"partial_update": self.client.patch(
self.detail_url,
{"last_name": "Byron"},
format="json",
),
"delete": self.client.delete(self.detail_url),
}
for operation, response in responses.items():
with self.subTest(operation=operation):
self.assertEqual(
response.status_code,
status.HTTP_403_FORBIDDEN,
)
What Does This Permission Test Cover?
The test sends anonymous create, replace, partial update, and delete requests. Every operation must return HTTP_403_FORBIDDEN.
- Save directory/tests.py.
- Verify the employee permission boundary by running this command:
python manage.py test
What Does This Check Prove?
The new test covers all employee write methods with an unauthenticated client. A passing result proves that no anonymous mutation route slips through.
The suite ends in OK. Both resources now deny anonymous writes consistently.
Employee Writes Not Returning 403?
Confirm that EmployeeViewSet sets permission_classes to IsAuthenticatedOrReadOnly. Make sure this test does not authenticate the client.
Check that each request uses the employee collection URL or detail URL from setUp(). Help me debug the employee permission test.
The authenticated employee flow exercises the same lifecycle with a relational payload. It also proves that updates keep the department reference valid.
- Add the authenticated employee create and replace flow below the anonymous write test by pasting this code:
def test_authenticated_user_can_create_update_and_delete_employee(self):
self.client.force_authenticate(user=self.user)
create_response = self.client.post(
self.list_url,
self.employee_payload(),
format="json",
)
self.assertEqual(create_response.status_code, status.HTTP_201_CREATED)
created_url = reverse(
"employee-detail",
args=[create_response.data["id"]],
)
replace_response = self.client.put(
created_url,
self.employee_payload(
first_name="Rear Admiral Grace",
email="grace.hopper@example.com",
),
format="json",
)
self.assertEqual(replace_response.status_code, status.HTTP_200_OK)
How Does the Employee Flow Begin?
- The authenticated client creates Grace Hopper using the reusable payload.
- The returned employee ID creates the detail URL for later requests.
- The PUT request replaces the record with a complete payload containing a new first name and email.
- Extend the authenticated employee method below its final assertion by pasting this code:
patch_response = self.client.patch(
created_url,
{"last_name": "Murray Hopper"},
format="json",
)
self.assertEqual(patch_response.status_code, status.HTTP_200_OK)
delete_response = self.client.delete(created_url)
self.assertEqual(delete_response.status_code, status.HTTP_204_NO_CONTENT)
self.assertFalse(
Employee.objects.filter(pk=create_response.data["id"]).exists()
)
How Does the Employee Flow Finish?
- The PATCH request changes only the last name.
- The delete request removes the created employee.
- The final ORM assertion proves the row has disappeared from the test database.
- Save directory/tests.py.
- Verify the authenticated employee lifecycle by running this command:
python manage.py test
What Does This Check Prove?
The test runner executes the complete authenticated employee lifecycle. The passing result confirms that every mutation method works with valid data.
The suite ends in OK. Employee CRUD behavior now has end-to-end API coverage.
Authenticated Employee Flow Failing?
Confirm that the payload uses self.department.pk for the department field. Check that the replacement payload changes Ada's existing email to the unique Grace Hopper email.
Verify that the final ORM query uses the ID from create_response. Help me debug the authenticated employee CRUD flow.
Successful requests prove the happy path. Validation tests deliberately send a duplicate email plus a department ID that does not exist.
- Add the employee validation test below the authenticated flow by pasting this code:
def test_employee_validation_rejects_duplicates_and_unknown_departments(self):
self.client.force_authenticate(user=self.user)
duplicate_response = self.client.post(
self.list_url,
self.employee_payload(email="ada@example.com"),
format="json",
)
unknown_department_response = self.client.post(
self.list_url,
self.employee_payload(
email="new@example.com",
department=999999,
),
format="json",
)
self.assertEqual(
duplicate_response.status_code,
status.HTTP_400_BAD_REQUEST,
)
self.assertIn("email", duplicate_response.data)
self.assertEqual(
unknown_department_response.status_code,
status.HTTP_400_BAD_REQUEST,
)
self.assertIn("department", unknown_department_response.data)
What Do the Validation Assertions Prove?
- The duplicate request reuses Ada's email and expects a bad-request response containing an email error.
- The unknown-department request uses 999999 and expects a bad-request response containing a department error.
- These checks prove that invalid JSON stops before an employee row is created.
- Save directory/tests.py.
✔️ Awesome, I've got everything!
Great. Double-check that you saved directory/tests.py before running the complete suite.
ⓧ I'd like to double check the full code
from django.contrib.auth import get_user_model
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from .models import Department, Employee
class PingAPITests(APITestCase):
def test_ping_returns_request_method(self):
response = self.client.get(reverse("ping"))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["method"], "GET")
class DepartmentAPITests(APITestCase):
def setUp(self):
self.user = get_user_model().objects.create_user(
username="backend-learner",
password="test-password",
)
self.department = Department.objects.create(name="Engineering")
self.list_url = reverse("department-list")
self.detail_url = reverse(
"department-detail",
args=[self.department.pk],
)
def test_anonymous_user_can_list_and_retrieve_departments(self):
list_response = self.client.get(self.list_url)
detail_response = self.client.get(self.detail_url)
self.assertEqual(list_response.status_code, status.HTTP_200_OK)
self.assertEqual(len(list_response.data), 1)
self.assertEqual(detail_response.status_code, status.HTTP_200_OK)
self.assertEqual(detail_response.data["name"], "Engineering")
def test_anonymous_user_cannot_write_departments(self):
responses = {
"create": self.client.post(
self.list_url,
{"name": "Finance"},
format="json",
),
"replace": self.client.put(
self.detail_url,
{"name": "Platform"},
format="json",
),
"partial_update": self.client.patch(
self.detail_url,
{"name": "Infrastructure"},
format="json",
),
"delete": self.client.delete(self.detail_url),
}
for operation, response in responses.items():
with self.subTest(operation=operation):
self.assertEqual(
response.status_code,
status.HTTP_403_FORBIDDEN,
)
def test_authenticated_user_can_create_update_and_delete_department(self):
self.client.force_authenticate(user=self.user)
create_response = self.client.post(
self.list_url,
{"name": "Finance"},
format="json",
)
self.assertEqual(create_response.status_code, status.HTTP_201_CREATED)
created_url = reverse(
"department-detail",
args=[create_response.data["id"]],
)
replace_response = self.client.put(
created_url,
{"name": "People Operations"},
format="json",
)
self.assertEqual(replace_response.status_code, status.HTTP_200_OK)
patch_response = self.client.patch(
created_url,
{"name": "Operations"},
format="json",
)
self.assertEqual(patch_response.status_code, status.HTTP_200_OK)
delete_response = self.client.delete(created_url)
self.assertEqual(delete_response.status_code, status.HTTP_204_NO_CONTENT)
self.assertFalse(
Department.objects.filter(pk=create_response.data["id"]).exists()
)
class EmployeeAPITests(APITestCase):
def setUp(self):
self.user = get_user_model().objects.create_user(
username="backend-learner",
password="test-password",
)
self.department = Department.objects.create(name="Engineering")
self.employee = Employee.objects.create(
first_name="Ada",
last_name="Lovelace",
email="ada@example.com",
hired_on="2024-01-15",
department=self.department,
)
self.list_url = reverse("employee-list")
self.detail_url = reverse(
"employee-detail",
args=[self.employee.pk],
)
def employee_payload(self, **overrides):
payload = {
"first_name": "Grace",
"last_name": "Hopper",
"email": "grace@example.com",
"hired_on": "2024-02-01",
"department": self.department.pk,
}
payload.update(overrides)
return payload
def test_anonymous_user_can_list_and_retrieve_employees(self):
list_response = self.client.get(self.list_url)
detail_response = self.client.get(self.detail_url)
self.assertEqual(list_response.status_code, status.HTTP_200_OK)
self.assertEqual(len(list_response.data), 1)
self.assertEqual(detail_response.status_code, status.HTTP_200_OK)
self.assertEqual(detail_response.data["email"], "ada@example.com")
def test_anonymous_user_cannot_write_employees(self):
responses = {
"create": self.client.post(
self.list_url,
self.employee_payload(),
format="json",
),
"replace": self.client.put(
self.detail_url,
self.employee_payload(email="ada@example.com"),
format="json",
),
"partial_update": self.client.patch(
self.detail_url,
{"last_name": "Byron"},
format="json",
),
"delete": self.client.delete(self.detail_url),
}
for operation, response in responses.items():
with self.subTest(operation=operation):
self.assertEqual(
response.status_code,
status.HTTP_403_FORBIDDEN,
)
def test_authenticated_user_can_create_update_and_delete_employee(self):
self.client.force_authenticate(user=self.user)
create_response = self.client.post(
self.list_url,
self.employee_payload(),
format="json",
)
self.assertEqual(create_response.status_code, status.HTTP_201_CREATED)
created_url = reverse(
"employee-detail",
args=[create_response.data["id"]],
)
replace_response = self.client.put(
created_url,
self.employee_payload(
first_name="Rear Admiral Grace",
email="grace.hopper@example.com",
),
format="json",
)
self.assertEqual(replace_response.status_code, status.HTTP_200_OK)
patch_response = self.client.patch(
created_url,
{"last_name": "Murray Hopper"},
format="json",
)
self.assertEqual(patch_response.status_code, status.HTTP_200_OK)
delete_response = self.client.delete(created_url)
self.assertEqual(delete_response.status_code, status.HTTP_204_NO_CONTENT)
self.assertFalse(
Employee.objects.filter(pk=create_response.data["id"]).exists()
)
def test_employee_validation_rejects_duplicates_and_unknown_departments(self):
self.client.force_authenticate(user=self.user)
duplicate_response = self.client.post(
self.list_url,
self.employee_payload(email="ada@example.com"),
format="json",
)
unknown_department_response = self.client.post(
self.list_url,
self.employee_payload(
email="new@example.com",
department=999999,
),
format="json",
)
self.assertEqual(
duplicate_response.status_code,
status.HTTP_400_BAD_REQUEST,
)
self.assertIn("email", duplicate_response.data)
self.assertEqual(
unknown_department_response.status_code,
status.HTTP_400_BAD_REQUEST,
)
self.assertIn("department", unknown_department_response.data)
Run the complete contract
The complete suite now covers every router method plus both invalid employee payloads. Its final run proves the API contract as one unit.
Before you run the complete suite, do you expect all eight tests to reach the permission and validation results you defined?
- Run the complete endpoint and permission suite by running this command:
python manage.py test
What Does the Final Command Prove?
- Django discovers every test method in PingAPITests, DepartmentAPITests, and EmployeeAPITests.
- The test runner creates an isolated database for the fixtures and API requests.
- The runner destroys that test database after completion so db.sqlite3 remains separate.
You should see Ran 8 tests followed by OK. That result proves the full request, validation, database, and permission contract.
Complete Suite Not Passing?
Read the first failing test name in the terminal. Match that name to the corresponding method in directory/tests.py.
Compare the reported status or response field with the assertion directly beneath its request. Help me diagnose the first failing API test.
- Return to directory/tests.py in the editor.
- Match each test method name to the HTTP method used inside it.
- Trace each reverse() name to its router-generated collection route or detail route.
Why Do Anonymous Writes Return 403?
The project configures SessionAuthentication as its authentication scheme. An anonymous request has no authenticated session.
The IsAuthenticatedOrReadOnly permission allows anonymous GET, HEAD, and OPTIONS requests. It blocks anonymous write methods.
Denied unauthenticated requests use HTTP 403 Forbidden under this authentication scheme. The authenticated tests use force_authenticate to cross that boundary.
That is the hard part complete. Your eight passing tests now prove every CRUD method plus the authorization rules around them.
Your employee directory now has an executable safety net. Future backend changes can be checked against the same eight endpoint contracts.
Secret mission
Filter Employees by Department
Add department filtering to the employee collection through a query parameter. Then protect the behavior with a ninth API test.
Clean Up Your Resources
Clean Up Your Resources
Choose whether to keep your local work available, pause the running tools, or delete the project entirely. This local Django plus SQLite project has no ongoing costs.
Resources you used:
- A local .venv virtual environment containing Django 6.1.2 plus Django REST Framework 3.18.3.
- A local employee-management-api folder containing the project source files.
- A local db.sqlite3 database containing the migrated Department plus Employee schema.
Keep everything running
No action is needed while you are actively building. The local server can stay available for more endpoint work.
- Leave the employee-management-api folder in its current location.
- Keep the .venv virtual environment with its pinned packages.
- Retain db.sqlite3 as your local data baseline for the PostgreSQL plus JWT follow-on project.
Pause - I'll come back to this later
Shut down the development server to free up local resources. Your project files plus SQLite data stay ready for your return.
- Switch back to the terminal panel in Visual Studio Code that ran the local server.
- Stop the local server by pressing Ctrl-C.
- Exit the active virtual environment by running this command:
deactivate
What does deactivation change?
The deactivate command returns the terminal to your system Python. It preserves the virtual environment plus every project file.
Your employee directory is safely paused. The code plus SQLite data remain ready for your return.
Delete - I don't want to use this again
Deleting the folder is permanent. It affects only this local project plus the resources stored inside it.
- Switch back to the terminal panel in Visual Studio Code that ran the local server.
- Stop the local server by pressing Ctrl-C.
- Exit the active virtual environment by running this command:
deactivate
What does deactivation change?
The deactivate command closes the terminal session's connection to .venv. It does not delete any files.
- Remove the entire employee-management-api folder by running these commands:
cd ..
rm -rf employee-management-api
What do these commands remove?
The first command moves the terminal up one folder. The second permanently removes employee-management-api with .venv plus db.sqlite3 inside it.
You should return to the terminal prompt without an error. You now have a clean slate.
Nice Work!
Nice Work!
You did it! Your tested employee directory now serves departments and employees through a Django REST Framework CRUD API. Anonymous clients can read records. Authenticated clients can create, replace, update, and delete them.
You've learned how to:
- Trace the HTTP request flow from a URL to a view. Return the result as a JSON response.
- Build a relational data model for departments and employees. Inspect the generated migration SQL before applying it to SQLite.
- Prove the API contract with nine APITestCase tests covering CRUD methods, validation failures, and permission boundaries.
- Secret Mission: Filter employees by department through a query parameter. Protect that behavior with a regression test.
Ready to quiz yourself?