惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

云风的 BLOG
云风的 BLOG
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
IT之家
IT之家
Recent Announcements
Recent Announcements
B
Blog
D
Docker
V
V2EX
GbyAI
GbyAI
L
LangChain Blog
博客园 - Franky
U
Unit 42
T
The Blog of Author Tim Ferriss
A
About on SuperTechFans
博客园 - 【当耐特】
Google DeepMind News
Google DeepMind News
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Vercel News
Vercel News
博客园_首页
D
DataBreaches.Net
人人都是产品经理
人人都是产品经理
Y
Y Combinator Blog
量子位
Blog — PlanetScale
Blog — PlanetScale
罗磊的独立博客

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Day 89 of #100DaysOfCode — DevCollab: Building the Projec...
M Saad Ahmad · 2026-05-02 · via DEV Community

Yesterday, the database was completed; all models defined, all migrations applied, all test data seeded. Today, I built the Projects API on top of that foundation. Serializers, a ViewSet with custom permissions, search, and filter, and a dedicated endpoint for a user's own projects. By the end of today, every project-related endpoint was tested and working in Postman.


The Approach: ViewSet Over Individual Views

For the project's API, there were two options: write individual function-based or class-based views for each endpoint, or use a ModelViewSet that handles all CRUD operations in one class.

I went with ModelViewSet. The reason is straightforward: all six standard operations on a project: list, create, retrieve, update, partial update, delete; follow the same pattern. ViewSet handles that pattern automatically, and the router generates all the URL patterns from a single registration line. The only code I needed to write was the parts that deviate from the default: custom permissions, custom queryset filtering, and a non-standard mine action.

This is exactly the DRF design philosophy: write the parts that are unique to your app, let the framework handle the boilerplate.


Serializers — More Than Just Field Mapping

The ProjectSerializer does more than map model fields to JSON. Three things make it interesting.

First, the tech_stack_list and roles_list fields. The database stores these as comma-separated strings for simplicity. The API should return them as proper arrays; that's what any frontend or API consumer expects. SerializerMethodField handles this: a method on the serializer calls the model's helper method and returns the result as a list. The raw string field is also included in the serializer for write operations. When creating or updating a project, the frontend sends a comma-separated string, and when reading, it gets back both the raw string and the parsed list.

Second, the nested owner data. The project detail response includes the owner's profile information: username, avatar, skills, GitHub URL, not just the owner's ID. This means a single request to /api/projects/1/ gives the frontend everything it needs to render the project detail page without a second request to fetch the owner's profile. SerializerMethodField handles this too, using the UserSerializer from the accounts app to serialize the owner.

Third, the request_status field for authenticated users. When a logged-in user views a project, the serializer checks whether that user has already sent a collaboration request for it and includes the status: pending, accepted, rejected, or null if no request exists. The frontend uses this to decide what to show in the apply button area. This field requires access to the current request object, which is passed through the serializer context from the view.


Permissions — The IsOwner Class

Django REST Framework's built-in permission classes handle authentication; IsAuthenticated confirms a user is logged in. But DevCollab needs a second layer: confirming that the logged-in user is the owner of the specific object they're trying to modify.

The IsOwner custom permission class handles this. It implements has_object_permission: a method DRF calls automatically for single-object operations like retrieve, update, and delete. It checks whether request.user matches obj.owner. If they match, the operation proceeds. If they don't, DRF returns a 403 Forbidden response.

The has_permission method handles request-level permission, before DRF even fetches the object. Safe methods like GET and HEAD always pass. Write methods require authentication.

This two-level approach, request-level and object-level, means unauthenticated users get a 401, authenticated non-owners get a 403, and owners get through. Each error code communicates exactly what the problem is.


The ViewSet — Bringing the Pieces Together

The ProjectViewSet is where everything connects. Four customized behaviors on top of what ModelViewSet provides automatically.

get_queryset: the default queryset returns all projects. DevCollab's version adds filtering. If a search query parameter is present, it filters across title, description, and tech stack using Q objects with OR logic and icontains for case-insensitive matching. If a tech_stack parameter is present, it filters projects where the tech stack contains that value. If a role parameter is present, it filters on the roles needed. Multiple filters stack; a request with both search and tech_stack gets both applied simultaneously. The base queryset only includes projects where is_open is True for the public list; the mine action gets all of the user's projects regardless of open status.

get_permissions: different actions need different permissions. List and retrieve are public; no authentication needed, anyone can browse projects. Create requires authentication; you must be logged in to post a project. Update, partial update, and delete require both authentication and ownership; you must be logged in, and you must own the project. The mine action requires authentication only. get_permissions returns the right combination of permission classes based on self.action.

perform_create: When a project is created, the owner is set to request.user automatically. The owner field is not in the serializer's writable fields — it's always set in the view, never accepted from the request body. This prevents one user from creating a project and attributing it to another user.

The mine action: a custom action decorated with @action(detail=False, methods=['get']) that returns only the projects owned by the current user. It bypasses the is_open filter, so owners see all their projects, including closed ones. The URL is /api/projects/mine/ and it requires authentication.


Search and Filter in Practice

The search and filter system deserves its own explanation because it's the feature users will interact with most on the browse page.

A request to /api/projects/?search=django returns projects where "django" appears anywhere in the title, description, or tech stack, case insensitive. A request to /api/projects/?tech_stack=react returns projects where React is listed in the tech stack. Both can be combined: /api/projects/?search=portfolio&tech_stack=nextjs returns projects about portfolios that use Next.js.

The icontains lookup is important here; it uses SQL LIKE under the hood, which means partial matches work. Searching for "django" matches "Django REST Framework" in the tech stack. This is the behavior users expect from a search box.

Q objects handle the OR logic for keyword search; the same keyword is checked against multiple fields simultaneously, and a project is included if the keyword appears in any of them. Without Q objects, a filter would require the keyword to appear in all fields at once, which is never what you want for search.


The Router

The router is one of DRF's most satisfying features. Registering ProjectViewSet with the router generates all of these URL patterns automatically:

The list and create endpoint on the collection URL, the retrieve, update, partial update, and delete endpoints on the detail URL with the project ID, and the custom mine action on its own URL. Six standard endpoints and one custom one, all from a single router.register() call.

The router is included in the main project URLs under the /api/ prefix, so all project endpoints live under /api/projects/.


Testing in Postman

Every endpoint was tested in Postman before calling it done today. The test sequence matters; some endpoints depend on data created by others.

First, register a user and copy the access token. Then create a project with that token. Then, retrieve the project list without a token to confirm it's public. Then try to update the project without a token and confirm you get a 401. Then try to update it with a different user's token and confirm you get a 403. Then update it with the owner's token and confirm it works. Then hit the mine endpoint and confirm only your projects appear. Then search for the project by title and confirm it appears in the results. Then filter by tech stack and confirm the filter works.

Testing the failure cases: 401, 403, 404, is as important as testing the success cases. The permission system is only trustworthy if you've verified it rejects what it should reject.


Where Things Stand After Day 89

The project's API is complete and tested. Every endpoint works, permissions are enforced correctly, search and filter return the right results, and the mine endpoint gives owners a view of all their projects.

Tomorrow: the collaboration requests API: sending requests, listing them for a project, accepting and rejecting, and viewing your own sent requests. That completes the entire backend.

Thanks for reading. Feel free to share your thoughts!