DEV Community

Cover image for Day 90 of #100DaysOfCode — DevCollab: Collaboration Requests API and Search
M Saad Ahmad
M Saad Ahmad

Posted on

Day 90 of #100DaysOfCode — DevCollab: Collaboration Requests API and Search

Yesterday, the Projects API was completed and tested. Today I built the final piece of the Django backend: the Collaboration Requests API. This is the feature that makes DevCollab a platform rather than just a directory. Developers send requests, owners review and respond, and both sides can track the status. By the end of today, the entire backend is complete, tested, and ready for the Next.js frontend to be built on top of it.


What the Collaboration Requests API Needs to Do

Before writing any code, it's worth being precise about what this API needs to handle, because the permission logic here is more complex than anything built so far.

Four operations, each with distinct permission requirements:

Sending a request: any authenticated user can send a request to any project, as long as they are not the project owner and have not already sent a request to that project. The uniqueness constraint at the database level handles the duplicate case, but the view needs to handle the ownership case; an owner shouldn't be able to request to join their own project.

Listing requests for a project: only the project owner should see the full list of those who have requested to collaborate. A random authenticated user should not be able to see who has applied to someone else's project.

Accepting or rejecting a request, only the project owner can change a request's status. The requester cannot accept or reject their own request.

Viewing your own sent requests, any authenticated user can see all the requests they have sent across all projects, along with the current status of each. This is for the requester's own dashboard; they need to know if their requests have been reviewed.

Four operations, four distinct permission scenarios. Getting this right is the most important part of today.


The CollaborationRequestSerializer

The serializer for collaboration requests does more than serialize the model fields. It nests the requester's full profile data, the same pattern used in the ProjectSerializer for the owner. When a project owner views incoming requests, they need to see who is asking: username, skills, GitHub link, bio, not just a user ID. One request to the API gives them everything they need to evaluate the requester.

For the write side, the serializer only exposes the message field. The project, requester, and status are all set in the view, never from the request body. This is the same principle as the ProjectSerializer not accepting an owner from the request body. The view controls who owns what.

A SerializerMethodField for project_detail gives the requester's view of their sent requests the full project information: title, owner, tech stack, so their dashboard shows meaningful context rather than just IDs.


The Permission Logic

The collaboration request endpoints need two custom permission behaviors that don't exist in DRF's built-in classes.

The first is checking that the request sender is not the project owner. A project owner sending a request to their own project makes no sense; they already own it. This check happens in the view before the serializer runs, not in a permission class, because it requires comparing the current user to the project's owner field, which is fetched in the same view.

The second is checking that the user modifying a request status is the project owner, not the requester. The IsProjectOwner permission class handles this. It implements has_object_permission on the CollaborationRequest object, checking obj.project.owner == request.user rather than obj.owner == request.user. The distinction matters: the collaboration request object has a requester, but the permission we're checking is ownership of the project the request belongs to.


The ViewSet Structure

The collaboration request ViewSet is organized differently from the project ViewSet because collaboration requests are always accessed in the context of a project. The URL structure reflects this, requests live under /api/projects/<project_id>/requests/ rather than at their own top-level route.

This nested structure means the ViewSet needs to extract the project ID from the URL kwargs and use it to filter the queryset. Every operation starts by fetching the project and confirming it exists. If the project ID in the URL doesn't match any project, the view returns a 404 immediately.

The get_queryset method filters collaboration requests by the project ID from the URL. This means the list endpoint returns only requests for that specific project, not all requests in the system.

perform_create does three things: it confirms the current user is not the project owner, it checks for an existing request from this user on this project, and if both checks pass, it creates the request with the project and requester set automatically.

The status update action, accepting or rejecting, is a separate action rather than using the default partial_update. This keeps the update scope narrow: only the status field can be changed through this endpoint. Nothing else about a collaboration request should be editable after creation.


The Mine Endpoint

The mine endpoint lives outside the nested project URL structure; it's at /api/requests/mine/ rather than under a specific project. This is because it returns all requests sent by the current user across all projects, not requests for a specific project.

This endpoint is the requester's view of their activity, a list of every collaboration request they have sent, which project it was for, and what the current status is. The response includes full project details for each request so the frontend can display meaningful information, project title, owner name, and tech stack without additional API calls.


Handling the unique_together Constraint Gracefully

The unique_together constraint on the CollaborationRequest model, one request per user per project, is enforced at the database level. When a second request is attempted, the database raises an IntegrityError.

The view catches this error and returns a clean 400 response with a descriptive message rather than letting the error bubble up as a 500. This is an important distinction: the database constraint is the safety net, but the view should handle the case gracefully before it reaches the database by checking for an existing request first. If the check misses somehow, race condition, direct API call, the constraint catches it, and the view turns the database error into a clean API response.


Completing the URL Structure

After today, the complete URL map for the backend looks like this:

Authentication and profile endpoints under /api/auth/. Project endpoints under /api/projects/ with the router handling all standard CRUD patterns plus the mine action. Collaboration request endpoints nested under /api/projects/<id>/requests/ for project-scoped operations, plus /api/requests/mine/ for the requester's own view.

The router handles projects. Collaboration request URLs are written manually; the nested structure doesn't fit cleanly into the router's automatic URL generation. Manual path() definitions give more control over the exact URL shape.


Testing the Complete Backend

With the collaboration requests API done, today's Postman testing covers the full user journey end-to-end.

User A registers and creates a project. User B registers and sends a collaboration request to User A's project with a message. User A hits the requests endpoint for their project and sees User B's request with full profile data. User A accepts the request. User B hits the mine endpoint and sees their request now shows as accepted. User A tries to send a request to their own project and gets a 400. User B tries to accept or reject a request on User A's project and gets a 403. User B tries to send a second request to the same project and gets a 400.

Every success case and every failure case is tested. The backend is only trustworthy if the rejections work as reliably as the acceptances.


Where Things Stand After Day 90

The Django backend for DevCollab is complete. Every endpoint is defined, every permission is enforced, and every edge case is handled. The API covers user registration and authentication, profile management, full project CRUD with search and filter, and the complete collaboration request lifecycle.

Thanks for reading. Feel free too share your thougts!

Top comments (0)