<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: M Saad Ahmad</title>
    <description>The latest articles on Forem by M Saad Ahmad (@m_saad_ahmad).</description>
    <link>https://forem.com/m_saad_ahmad</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1853445%2F521468fd-cc24-4c92-9f2f-ec930c04557b.jpg</url>
      <title>Forem: M Saad Ahmad</title>
      <link>https://forem.com/m_saad_ahmad</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/m_saad_ahmad"/>
    <language>en</language>
    <item>
      <title>Day 100 of #100DaysOfCode — What I Built, What I Learned, and What's Next</title>
      <dc:creator>M Saad Ahmad</dc:creator>
      <pubDate>Fri, 15 May 2026 14:05:31 +0000</pubDate>
      <link>https://forem.com/m_saad_ahmad/day-100-of-100daysofcode-what-i-built-what-i-learned-and-whats-next-2007</link>
      <guid>https://forem.com/m_saad_ahmad/day-100-of-100daysofcode-what-i-built-what-i-learned-and-whats-next-2007</guid>
      <description>&lt;div class="crayons-card c-embed"&gt;

  &lt;br&gt;
Final Update &lt;strong&gt;#100DaysOfCode&lt;/strong&gt; is officially complete!&lt;br&gt;

&lt;/div&gt;


&lt;p&gt;Not perfectly. Not everything works. Not everything got finished. But 100 days of showing up, writing code, debugging it, shipping some of it, and learning from all of it, that part is done. And that was the point.&lt;/p&gt;




&lt;p&gt;Check out how I started: &lt;/p&gt;
&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/m_saad_ahmad/day-1-of-100daysofcode-react-refresher-tailwind-setup-57ja" class="crayons-story__hidden-navigation-link"&gt;Day 1 of #100DaysOfCode — React Refresher + Tailwind Setup&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/m_saad_ahmad" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1853445%2F521468fd-cc24-4c92-9f2f-ec930c04557b.jpg" alt="m_saad_ahmad profile" class="crayons-avatar__image" width="96" height="96"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/m_saad_ahmad" class="crayons-story__secondary fw-medium m:hidden"&gt;
              M Saad Ahmad
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                M Saad Ahmad
                
              
              &lt;div id="story-author-preview-content-3218116" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/m_saad_ahmad" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1853445%2F521468fd-cc24-4c92-9f2f-ec930c04557b.jpg" class="crayons-avatar__image" alt="" width="96" height="96"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;M Saad Ahmad&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/m_saad_ahmad/day-1-of-100daysofcode-react-refresher-tailwind-setup-57ja" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Feb 1&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/m_saad_ahmad/day-1-of-100daysofcode-react-refresher-tailwind-setup-57ja" id="article-link-3218116"&gt;
          Day 1 of #100DaysOfCode — React Refresher + Tailwind Setup
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/programming"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;programming&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/react"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;react&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/100daysofcode"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;100daysofcode&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/m_saad_ahmad/day-1-of-100daysofcode-react-refresher-tailwind-setup-57ja" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt; reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/m_saad_ahmad/day-1-of-100daysofcode-react-refresher-tailwind-setup-57ja#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              &lt;span class="hidden s:inline"&gt;Add Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            2 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;





&lt;h2&gt;
  
  
  Why I Did This
&lt;/h2&gt;

&lt;p&gt;Before day 1, I was stuck. Not stuck in the way where you don't know what to learn, stuck in the way where you know exactly what you want to learn, but you can never make yourself actually sit down and do it. I was on React. Had been on React for a long time. Not because React was hard, but because I was moving by mood and vibe. When I felt like coding, I'd code. When I didn't, I wouldn't. Weeks would pass. The learning stopped while the intention stayed exactly the same.&lt;/p&gt;

&lt;p&gt;100 days of code was a system I imposed on myself to fix that. The rule was simple: write code every day, write a blog post about it every day. The blog post part mattered as much as the code part. When someone might read about what you did today, "nothing" stops being an acceptable answer.&lt;/p&gt;

&lt;p&gt;The other reason was exposure. I had a vague sense that full-stack development involved a lot of moving parts: frontend frameworks, backend frameworks, databases, authentication, APIs, and deployment, but I had never touched most of them. I genuinely believed that if I wanted to build anything real, I needed years of learning before I'd know enough. The 100 days proved that belief wrong. Not because the learning was shallow, but because the velocity of learning-by-building is faster than I had given it credit for.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Journey: 100 Days in Brief
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Days 1–60: The JavaScript stack&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Started with React, moved to TypeScript in React, then Node.js and Express for backend, MongoDB and Mongoose for the database, and finished with Next.js. Sixty days covering the entire JavaScript full-stack. By the end of this stretch, I had built a task management app with Next.js and MongoDB, a tour listing app in React with TypeScript, and a full authentication system with React, Express, and MongoDB, registration, login, JWT-based sessions, and a dashboard showing registered users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Days 61–72: Python&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Came into this stretch overconfident. I'd used Python in university and thought a quick refresher was all I needed. Sat down to write a class and couldn't do it. Spent three days actually learning Python properly, functions, OOP, modules, exception handling, before touching Django at all. The humility check was necessary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Days 73–85: Django and Flask&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Built DevBoard, a job board application in Django, during the building phase of the Django learning. Then spent eight days learning Flask, routes, templates, SQLAlchemy, WTForms, and authentication with Flask-Login, and built a small expense tracker as the Flask capstone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Days 86–100: DevCollab&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The capstone project. Fourteen days building a full-stack developer collaboration platform from scratch, Django REST Framework backend, Next.js frontend, PostgreSQL database, deployed on Railway and Vercel.&lt;/p&gt;




&lt;h2&gt;
  
  
  Projects Built
&lt;/h2&gt;

&lt;p&gt;Not all of these are finished. Not all of them work perfectly. That's the honest truth, and I'm not going to hide it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Task Management App&lt;/strong&gt; — (&lt;a href="https://taskmanagementapp-nextjs.vercel.app/" rel="noopener noreferrer"&gt;Link&lt;/a&gt;)&lt;br&gt;
Built with Next.js and MongoDB. Users can create, update, and delete tasks. No authentication. Functional for what it is — a clean CRUD app that demonstrated the Next.js and MongoDB stack working together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tour Listing App&lt;/strong&gt;&lt;br&gt;
Built with React and TypeScript. Displays a list of tours with their details. A smaller project, but it was my first real TypeScript project, and the type safety caught several bugs during development that plain JavaScript would have let slip through.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authentication System&lt;/strong&gt; — (&lt;a href="https://auth-frontend-phi-vert.vercel.app/" rel="noopener noreferrer"&gt;Link&lt;/a&gt;)&lt;br&gt;
Built with React, Express.js, and MongoDB. Users can register and log in, and the dashboard shows the list of registered users. JWT-based authentication. This was the first project where a real backend connected to a real frontend — the moment things started feeling like full-stack development rather than just front-end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flask Expense Tracker&lt;/strong&gt; — (&lt;a href="https://expensetracker-production-828d.up.railway.app/expenses" rel="noopener noreferrer"&gt;Link&lt;/a&gt;)&lt;br&gt;
Built with Flask, Flask-SQLAlchemy, Flask-WTForms, and Flask-Login. Users can log expenses with amounts, categories, descriptions, and dates. See a filtered list and running total. A small but complete application that covers the full Flask stack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DevBoard: Django Job Board&lt;/strong&gt;&lt;br&gt;
Built with Django during the project-building phase of the Django learning section. Employers can sign up and post jobs. Candidates can sign up and apply to posted jobs. The concept is clean, and the employer side works. The candidate side has issues with the apply flow, the job listings page, and some of the routing logic isn't working correctly. There are bugs I'm actively debugging and working on. This one isn't live yet, and I'm not going to pretend it is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DevCollab: Developer Collaboration Platform&lt;/strong&gt; (&lt;a href="https://devcollab-frontend-one.vercel.app/" rel="noopener noreferrer"&gt;Link&lt;/a&gt;)&lt;br&gt;
The capstone. Details below.&lt;/p&gt;


&lt;h2&gt;
  
  
  DevCollab: The Capstone Project
&lt;/h2&gt;

&lt;p&gt;
  Click to see the Full Tech Stack
  &lt;ul&gt;
&lt;li&gt;Frontend: Next.js, Tailwind CSS&lt;/li&gt;
&lt;li&gt;Backend: Django REST Framework&lt;/li&gt;
&lt;li&gt;Database: PostgreSQL&lt;/li&gt;
&lt;li&gt;Auth: JWT (SimpleJWT)
&lt;/li&gt;
&lt;/ul&gt;




&lt;/p&gt;
&lt;p&gt;DevCollab is a platform where developers with project ideas can find collaborators, and developers looking to work on interesting projects can find them. You post a project with a description, the tech stack, and the roles you need. Other developers browse, find something they want to contribute to, and send a collaboration request with a message. The project owner reviews requests and accepts or rejects them. Everyone has a public profile showing their skills, bio, and active projects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Architecture&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two completely separate applications:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; Django + Django REST Framework, PostgreSQL database, deployed on Railway&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; Next.js (App Router), Tailwind CSS, deployed on Vercel&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They communicate exclusively over HTTP. The Next.js frontend makes API calls to the Django backend using Axios with JWT interceptors. The interceptor attaches the access token to every authenticated request automatically, and when the token expires, it silently refreshes it using the refresh token and retries the original request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authentication&lt;/strong&gt; is JWT-based using &lt;code&gt;djangorestframework-simplejwt&lt;/code&gt;. Access tokens expire in 30 minutes, refresh tokens in 7 days. Token rotation is enabled; every refresh generates a new refresh token and blacklists the old one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Django backend&lt;/strong&gt; exposes a REST API with these main resource groups: auth endpoints (register, login, logout, token refresh), user profile endpoints (get and update your own profile, view any public profile by username), project endpoints (full CRUD with search and filter by tech stack and role), and collaboration request endpoints (send, list, accept/reject, view your own sent requests). Custom permission classes enforce ownership; only project owners can edit or delete their projects, only project owners can accept or reject requests, and only the requester can view their own sent requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Next.js frontend&lt;/strong&gt; has nine main pages: a landing page, browse projects with search and filter, project detail with a four-state action button (login prompt for guests, edit/delete for owners, status badge for existing requesters, apply button for new requesters), create project, edit project, apply to a project, a dashboard for managing your projects and incoming requests, a public profile page, and an edit profile page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What features made it in and why&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The collaboration request lifecycle: send, accept, reject, and track status, is the core of the app, and it works. The project CRUD is complete. Authentication is solid. Public profiles work. Search and filter on the browse page work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Current status: live but work in progress&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;DevCollab is deployed and accessible. The core flows work. But there are bugs. The frontend has rough edges, some loading states are incomplete, some error messages are raw API text that didn't get replaced before deployment, and there are edge cases in the collaboration request flow that behave unexpectedly. The backend API is solid but hasn't been fully load-tested, and there are likely edge cases in the permission logic that haven't been hit yet.&lt;/p&gt;

&lt;p&gt;This is the honest state of it. It works well enough to demonstrate the concept and the architecture. It is not polished enough to put in front of real users and walk away. That work is ongoing.&lt;/p&gt;


&lt;h2&gt;
  
  
  Honest Lessons
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;Building takes longer than you think. Every time.&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;I came into this challenge with a mental model where building was the easy part: you learn the tools, you have an idea, you know what to build, you just build it. What I discovered is that this model is wrong in a specific and humbling way. Writing code is fast. Making code work is slow. The gap between "I have written the code" and "this actually does what I intended in all cases" is where the real time goes.&lt;/p&gt;

&lt;p&gt;DevBoard isn't live because the routing logic broke in ways I didn't anticipate. DevCollab has bugs that survived testing because I tested the happy path, but not the edge cases. Neither of these is a skill failure; they're a failure to budget realistic time for debugging and testing. AI can give you the code. Making it work, debugging it, and testing it properly is work that cannot be shortcut, AI or no AI.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;Overconfidence is the enemy of learning.&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The Python refresher situation on day 62 is the clearest example. I thought I knew Python. I didn't know Python. I wasted two days before admitting that to myself and starting properly. The same thing happened with Django's URL routing, and it's Django's migration system. I assumed I understood them and paid for it in debugging time when I was wrong. The pattern is always the same: overestimate your understanding, skip the fundamentals, then spend three times as long fixing the consequences.&lt;/p&gt;




&lt;div class="crayons-card c-embed"&gt;

  
&lt;h3&gt;
  
  
  &lt;strong&gt;The blog posts were as valuable as the code.&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;I learned that writing the blog post is just as important as writing the code for long-term retention.&lt;br&gt;

&lt;/p&gt;
&lt;/div&gt;


&lt;p&gt;Writing about what I built every day forced me to actually understand what I was doing rather than just copying it. You can copy code without understanding it. You cannot write a coherent explanation of code without understanding it. The posts were the accountability mechanism and the comprehension check at the same time.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Knowing tools is not the same as knowing how to use them.&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;By day 100, I had touched React, TypeScript, Next.js, Express, MongoDB, Django, Flask, PostgreSQL, DRF, JWT authentication, and deployment on Railway and Vercel. I know what all of these are, how they fit together, what problems they solve, and how to build with them at a basic to intermediate level. But none of that is the same as being able to build a polished, production-quality application with them. That comes from more projects, more debugging, more edge cases, and more time.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;A few months ago, I didn't know what to build. I had ideas, but didn't know how to build them and wasn't familiar with the necessary tools to execute them. I always thought that my lack of knowledge and understanding of how to use tools was holding me back. Now I have the tools. What I have instead of the old problem is a pile of unfinished business.&lt;/p&gt;

&lt;p&gt;DevBoard needs to be fixed and deployed. DevCollab needs its rough edges smoothed, its bugs resolved, and its missing features completed. These aren't new projects; they're commitments I made during this challenge that aren't fully honoured yet.&lt;/p&gt;

&lt;p&gt;That's the immediate focus. Finish what was started. Both applications are live, working, and presentable.&lt;/p&gt;

&lt;p&gt;After that, the plan is job hunting, offering freelance services, and continuing to build. The 100 days didn't make me a senior developer. They made me someone who knows enough to be dangerous, someone who can pick up a task, figure out what's needed, build something that mostly works, debug what doesn't, and ship it. That's enough to start.&lt;/p&gt;

&lt;p&gt;The 100 days are done. The work? Far from done.&lt;/p&gt;




&lt;p&gt;If you enjoyed this, follow me here: &lt;/p&gt;
&lt;div class="ltag__user ltag__user__id__1853445"&gt;
    &lt;a href="/m_saad_ahmad" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=150,height=150,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1853445%2F521468fd-cc24-4c92-9f2f-ec930c04557b.jpg" alt="m_saad_ahmad image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/m_saad_ahmad"&gt;M Saad Ahmad&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/m_saad_ahmad"&gt;Space science graduate | Learning MERN stack

Writing about what I build, break, and finally understand.

Focus: full-stack web dev, data workflows, and real-world learning.&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;I've been posting mostly in &lt;/p&gt;
&lt;div class="ltag__tag ltag__tag__id__225"&gt;
    &lt;div class="ltag__tag__content"&gt;
      &lt;h2&gt;#&lt;a href="https://dev.to/t/100daysofcode" class="ltag__tag__link"&gt;100daysofcode&lt;/a&gt; Follow
&lt;/h2&gt;
      &lt;div class="ltag__tag__summary"&gt;
        The 100 Days of Code is a coding challenge created by Alexander Kallaway to encourage people to learn new coding skills. 
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
 and &lt;div class="ltag__tag ltag__tag__id__8"&gt;
    &lt;div class="ltag__tag__content"&gt;
      &lt;h2&gt;#&lt;a href="https://dev.to/t/webdev" class="ltag__tag__link"&gt;webdev&lt;/a&gt; Follow
&lt;/h2&gt;
      &lt;div class="ltag__tag__summary"&gt;
        Because the internet...
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;You can also connect with me on &lt;a href="//www.linkedin.com/in/muhammad-saad-ahmad"&gt;LinkedIn&lt;/a&gt; as well. This is where I am active the most.&lt;/p&gt;




&lt;p&gt;Thanks for reading. Really appreciate anyone who was following the journey along.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>buildinpublic</category>
      <category>100daysofcode</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Day 99 of #100DaysOfCode — DevCollab: Deploying Next.js and Going Live</title>
      <dc:creator>M Saad Ahmad</dc:creator>
      <pubDate>Wed, 13 May 2026 17:55:25 +0000</pubDate>
      <link>https://forem.com/m_saad_ahmad/day-99-of-100daysofcode-devcollab-deploying-nextjs-and-going-live-45fc</link>
      <guid>https://forem.com/m_saad_ahmad/day-99-of-100daysofcode-devcollab-deploying-nextjs-and-going-live-45fc</guid>
      <description>&lt;p&gt;Today, DevCollab went fully live. The Next.js frontend is deployed on Vercel, the Django backend is on Railway, they're talking to each other over the internet, and anyone with the link can use the app. The last 99 days of building, learning, debugging, and shipping came down to this.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deploying to Vercel
&lt;/h2&gt;

&lt;p&gt;Deploying the Next.js frontend to Vercel was the simplest deployment of the entire project. Vercel was built for Next.js; it's made by the same team. The process was connecting the GitHub repository, adding one environment variable, and clicking deploy.&lt;/p&gt;

&lt;p&gt;The build took about two minutes. Vercel ran &lt;code&gt;npm run build&lt;/code&gt; against the repository, detected the App Router configuration, optimized all the pages, and produced a deployment. The same build that was tested locally on day 98 passed on Vercel without any changes. That's the value of running the production build locally first, no surprises.&lt;/p&gt;

&lt;p&gt;The environment variable is the one critical piece. &lt;code&gt;NEXT_PUBLIC_API_URL&lt;/code&gt; pointing to the Railway backend URL is the entire connection between the frontend and the backend. Getting this wrong, a missing &lt;code&gt;/api&lt;/code&gt; suffix, a trailing slash where there shouldn't be one, &lt;code&gt;http&lt;/code&gt; instead of &lt;code&gt;https&lt;/code&gt;, breaks every single API call silently. Vercel's environment variable dashboard is straightforward but worth triple-checking before deploying.&lt;/p&gt;




&lt;h2&gt;
  
  
  The CORS Fix That Always Gets Forgotten
&lt;/h2&gt;

&lt;p&gt;The first thing I did after the Vercel deployment succeeded was open the live URL and try to register. The registration failed. The browser console showed a CORS error. The Railway backend was blocking requests from the Vercel domain because it wasn't in the allowed origins list.&lt;/p&gt;

&lt;p&gt;This is the most commonly forgotten step in any full-stack deployment. The backend was configured with &lt;code&gt;CORS_ALLOWED_ORIGINS&lt;/code&gt; pointing at &lt;code&gt;localhost:3000&lt;/code&gt; for development. The live Vercel URL, &lt;code&gt;https://devcollab.vercel.app&lt;/code&gt; or whatever Vercel assigns, was not in that list.&lt;/p&gt;

&lt;p&gt;The fix is a one-line change to an environment variable in Railway's dashboard. Add the Vercel URL to &lt;code&gt;CORS_ALLOWED_ORIGINS&lt;/code&gt;. Railway redeploys automatically when environment variables change. Two minutes later, registration worked.&lt;/p&gt;

&lt;p&gt;The lesson is to always update CORS before testing the live app, not after discovering it's broken.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing the Live App from Scratch
&lt;/h2&gt;

&lt;p&gt;With CORS fixed, I opened the live URL on my phone, a device that had never touched this app, with no local state, no cached tokens, no pre-existing data. This is the most honest test possible.&lt;/p&gt;

&lt;p&gt;Everything worked. Registration created a real user in a real PostgreSQL database on Railway. The profile edit saved correctly. Creating a project made it appear on the browse page immediately. Searching for it by tech stack returned the right results. Opening the app in another browser, registering as a second user, and sending a collaboration request showed the pending badge on the detail page. Back on the first account, the dashboard showed the incoming request. Accepting it updated the status in place. The second account's sent requests page showed the accepted status.&lt;/p&gt;

&lt;p&gt;The full loop, the entire reason DevCollab exists, worked on a live production URL being hit from a real device.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Broke and What Was Fixed
&lt;/h2&gt;

&lt;p&gt;Two things broke during live testing that didn't break during the Railway-connected local testing on day 98.&lt;/p&gt;

&lt;p&gt;The first was the token refresh flow. Locally, the Next.js dev server and the Django server were both on localhost, so cookies and headers behaved consistently. On the live deployment, the Vercel frontend makes requests to a completely different domain on Railway. The &lt;code&gt;Authorization&lt;/code&gt; header was being set correctly, but one of the Axios interceptor configurations had a &lt;code&gt;withCredentials: true&lt;/code&gt; setting left over from an earlier attempt at cookie-based auth. On cross-domain requests, this caused the browser to add extra CORS preflight requirements that the backend wasn't configured to handle. Removing &lt;code&gt;withCredentials: true&lt;/code&gt; fixed it.&lt;/p&gt;

&lt;p&gt;The second was the project detail page flashing a "not found" error for about half a second before the data loaded. This happened because the page was checking &lt;code&gt;if (!project)&lt;/code&gt; to show the not-found message, but on the first render, before the fetch completed, &lt;code&gt;project&lt;/code&gt; was null, so it briefly showed the error. Adding a &lt;code&gt;loading&lt;/code&gt; state check before the not-found check, only shows not-found if loading is false AND project is null, fixed the flash.&lt;/p&gt;




&lt;h2&gt;
  
  
  Adding the Live URL to GitHub
&lt;/h2&gt;

&lt;p&gt;With everything working, the live URL was added to both GitHub repositories, in the repository description and in the README. The README got a brief section explaining what DevCollab is, the tech stack, and a link to the live app.&lt;/p&gt;

&lt;p&gt;This is worth doing properly. A recruiter or developer who finds the GitHub repo should be able to understand what the project is and click to the live app in under 30 seconds. A repository with no description and no README is a missed opportunity.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Things Stand After Day 99
&lt;/h2&gt;

&lt;p&gt;DevCollab is live. Two domains, one app:&lt;/p&gt;

&lt;p&gt;Backend: Django + DRF + PostgreSQL on Railway&lt;br&gt;
Frontend: Next.js on Vercel&lt;/p&gt;

&lt;p&gt;Thanks for reading. Feel free to share your thoughts!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>buildinpublic</category>
      <category>100daysofcode</category>
    </item>
    <item>
      <title>Day 98 of #100DaysOfCode — DevCollab: Finishing the Frontend and Testing Against the Live Backend</title>
      <dc:creator>M Saad Ahmad</dc:creator>
      <pubDate>Tue, 12 May 2026 08:40:24 +0000</pubDate>
      <link>https://forem.com/m_saad_ahmad/day-98-of-100daysofcode-devcollab-finishing-the-frontend-and-testing-against-the-live-backend-23a6</link>
      <guid>https://forem.com/m_saad_ahmad/day-98-of-100daysofcode-devcollab-finishing-the-frontend-and-testing-against-the-live-backend-23a6</guid>
      <description>&lt;p&gt;Today was the last day of writing code. The Next.js frontend got its final pieces, then the entire app was pointed at the live Railway backend and tested end-to-end with real data flowing between two deployed services. Tomorrow is purely deployment. Today was making sure there's something worth deploying.&lt;/p&gt;




&lt;h2&gt;
  
  
  Finishing the Frontend
&lt;/h2&gt;

&lt;p&gt;The last remaining pages and components got finished today. Nothing major, the core of the app has been working for several days. What was left was the edit profile form, the sent requests page, and a few component polish items that had been deferred. Getting these done today meant tomorrow's deployment wouldn't be interrupted by code that still needed writing.&lt;/p&gt;

&lt;p&gt;The rule I applied when deciding what to finish and what to cut was simple: if the app's core loop works without it, cut it. The core loop is register, post a project, another user applies, and the owner accepts. Everything that supports that loop stayed. Anything decorative or supplementary that wasn't built yet got cut.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pointing at the Live Backend
&lt;/h2&gt;

&lt;p&gt;The single most important change today was updating &lt;code&gt;.env.local&lt;/code&gt; to point &lt;code&gt;NEXT_PUBLIC_API_URL&lt;/code&gt; at the Railway URL instead of &lt;code&gt;localhost:8000&lt;/code&gt;. One line changed, and suddenly the Next.js app running on my machine was talking to a real PostgreSQL database on Railway's servers instead of a local SQLite file.&lt;/p&gt;

&lt;p&gt;This felt different immediately. The first registration on the live backend created a real user in a real production database. The project I created from the frontend showed up in the Django admin panel on Railway. The data is real now.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the End-to-End Test Revealed
&lt;/h2&gt;

&lt;p&gt;Running the full user flow with the live backend connected revealed four things that didn't show up during local testing.&lt;/p&gt;

&lt;p&gt;The first was a CORS issue. The Railway backend was configured with &lt;code&gt;CORS_ALLOWED_ORIGINS&lt;/code&gt; pointing to &lt;code&gt;http://localhost:3000&lt;/code&gt; for development. When the Next.js app on &lt;code&gt;localhost:3000&lt;/code&gt; made a request to the Railway URL, the browser blocked it because the allowed origins list was correct, but the actual Railway domain also needed to be in the list for cross-origin requests from localhost to Railway to work. Adding &lt;code&gt;http://localhost:3000&lt;/code&gt; back confirmed it was already there. The issue was actually the HTTPS vs HTTP mismatch. Railway serves over HTTPS, but the CORS config had &lt;code&gt;http://&lt;/code&gt;. Changing to &lt;code&gt;https://&lt;/code&gt; fixed it.&lt;/p&gt;

&lt;p&gt;The second was an avatar URL issue. Locally, avatars uploaded to the Django backend were served at &lt;code&gt;localhost:8000/media/avatars/filename.jpg&lt;/code&gt;. In production, the &lt;code&gt;MEDIA_URL&lt;/code&gt; was the same relative path, but Django on Railway doesn't automatically serve media files; WhiteNoise only handles static files. Uploaded avatars returned 404. For now, the avatar upload is disabled in the frontend with a note that it requires cloud storage. The profile works perfectly without it; it just shows the placeholder avatar. This is an acceptable trade-off for a portfolio project.&lt;/p&gt;

&lt;p&gt;The third was a field name mismatch. The collaboration request serializer returns the requester's profile data nested under &lt;code&gt;requester_data&lt;/code&gt;, but one component in the frontend was looking for &lt;code&gt;requester&lt;/code&gt; without the &lt;code&gt;_data&lt;/code&gt; suffix. This worked locally because I had old test data in the local database that happened to have the right shape. Against the live backend with fresh data, the field was missing, and the component showed nothing. A one-line fix in the component.&lt;/p&gt;

&lt;p&gt;The fourth was token refresh timing. The access tokens are set to expire after 30 minutes. During local testing, I never stayed on the app long enough to hit that expiry. The first time I left the live-connected app open for 35 minutes and came back, every API call returned 401 until the refresh interceptor kicked in. The interceptor worked correctly; it refreshed the token and retried the request, but there was a brief flash of an error toast before the retry succeeded. This is a known limitation of the current interceptor implementation and is acceptable for now.&lt;/p&gt;




&lt;h2&gt;
  
  
  Running the Production Build
&lt;/h2&gt;

&lt;p&gt;Before calling today done, &lt;code&gt;npm run build&lt;/code&gt; was run locally. This is the same process Vercel runs during deployment. If it passes locally, Vercel deployment has a very high chance of succeeding without surprises.&lt;/p&gt;

&lt;p&gt;The first build attempt failed with three errors. Two were unused imports that Next.js's production build is stricter about than the development server. The third was a missing &lt;code&gt;key&lt;/code&gt; prop on a list render that development mode warned about, but didn't block. All three were quick fixes. The second build attempt passed cleanly.&lt;/p&gt;

&lt;p&gt;Running the production build locally before deploying is a habit worth forming on every project. Vercel's build logs are readable, but debugging them remotely is slower than catching the errors on your own machine first.&lt;/p&gt;




&lt;h2&gt;
  
  
  Preparing the Environment Variables for Vercel
&lt;/h2&gt;

&lt;p&gt;Vercel needs to know the environment variables the Next.js app requires. These are the same variables in &lt;code&gt;.env.local&lt;/code&gt; but they need to be entered in Vercel's dashboard rather than in a file. Going through &lt;code&gt;.env.local&lt;/code&gt; and listing every variable that starts with &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; confirmed there's only one: &lt;code&gt;NEXT_PUBLIC_API_URL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For tomorrow, that value will be the Railway URL that the Railway assigned to the project. The variable is already in the code wherever the API base URL is needed, so changing it from a localhost address to a live URL requires zero code changes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Things Stand After Day 98
&lt;/h2&gt;

&lt;p&gt;The frontend is complete. Every planned page exists, every core user flow works, and the production build passes. The app has been tested end-to-end against the live backend, and four issues were found and resolved. The environment variable needed for Vercel deployment is documented and ready.&lt;/p&gt;

&lt;p&gt;Thanks for reading. Feel free to share your thoughts!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>buildinpublic</category>
      <category>100daysofcode</category>
    </item>
    <item>
      <title>Day 97 of #100DaysOfCode — DevCollab: Deploying the Django Backend to Railway</title>
      <dc:creator>M Saad Ahmad</dc:creator>
      <pubDate>Mon, 11 May 2026 10:56:38 +0000</pubDate>
      <link>https://forem.com/m_saad_ahmad/day-97-of-100daysofcode-devcollab-deploying-the-django-backend-to-railway-4go</link>
      <guid>https://forem.com/m_saad_ahmad/day-97-of-100daysofcode-devcollab-deploying-the-django-backend-to-railway-4go</guid>
      <description>&lt;p&gt;Today, the Django backend went live. Real URL, real PostgreSQL database, real API responses. Everything that was only accessible at &lt;code&gt;localhost:8000&lt;/code&gt; is now on the internet. It took longer than expected, it always does, but by the end of today, every endpoint was tested and working against the live Railway URL.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Railway
&lt;/h2&gt;

&lt;p&gt;Railway is the simplest Django deployment option available right now. It handles the server infrastructure, lets you provision a PostgreSQL database with one click, reads your environment variables from a dashboard, and deploys directly from GitHub. No Dockerfile needed, no Nginx configuration, no server management. For a portfolio project this size, it's the right tool.&lt;/p&gt;

&lt;p&gt;The free tier has usage limits, but it is more than enough for demonstrating a project. If DevCollab ever needed to handle real traffic, the deployment would need revisiting, but for portfolio purposes, Railway is exactly what's needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Preparing Django for Production
&lt;/h2&gt;

&lt;p&gt;Before touching Railway, the codebase needed a few changes to be production-ready.&lt;/p&gt;

&lt;p&gt;The settings file was split into base, development, and production. Everything shared between environments lives in the base. Development has &lt;code&gt;DEBUG=True&lt;/code&gt;, SQLite, and local CORS origins. Production has &lt;code&gt;DEBUG=False&lt;/code&gt;, PostgreSQL via &lt;code&gt;DATABASE_URL&lt;/code&gt;, the proper &lt;code&gt;ALLOWED_HOSTS&lt;/code&gt; and &lt;code&gt;CORS_ALLOWED_ORIGINS&lt;/code&gt; from environment variables, and all the security settings Django's deployment check recommends.&lt;/p&gt;

&lt;p&gt;WhiteNoise was added to serve static files directly from Django without a separate web server. In production, Django can't serve static files itself; WhiteNoise middleware handles that job cleanly without any additional infrastructure. It sits in the middleware stack just after Django's SecurityMiddleware, and the storage backend is set to WhiteNoise's compressed manifest version, which fingerprints file names for cache busting.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;Procfile&lt;/code&gt; was added to tell Railway what command to run to start the server. It runs &lt;code&gt;gunicorn&lt;/code&gt;, a production WSGI server that handles concurrent requests properly, unlike Django's built-in development server, which should never be used in production.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gunicorn&lt;/code&gt; and &lt;code&gt;whitenoise&lt;/code&gt; were both added to &lt;code&gt;requirements.txt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;python-decouple&lt;/code&gt; already handled all the sensitive configuration, so no hardcoded secrets needed to be removed; they were already in &lt;code&gt;.env&lt;/code&gt;, which is in &lt;code&gt;.gitignore&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Railway Setup
&lt;/h2&gt;

&lt;p&gt;The Railway deployment process has a specific order that matters.&lt;/p&gt;

&lt;p&gt;The PostgreSQL database gets provisioned first, before the Django service, because Django needs the &lt;code&gt;DATABASE_URL&lt;/code&gt; environment variable to exist before it can run migrations. Railway generates this URL automatically when you create a PostgreSQL instance, and it appears in the database's variables tab, ready to copy into the Django service's environment.&lt;/p&gt;

&lt;p&gt;The Django service is connected to the GitHub repository. Railway detects the &lt;code&gt;Procfile&lt;/code&gt; and uses it as the start command. The build command runs &lt;code&gt;pip install -r requirements.txt&lt;/code&gt; and &lt;code&gt;python manage.py collectstatic --noinput&lt;/code&gt; automatically.&lt;/p&gt;

&lt;p&gt;All environment variables are added in Railway's dashboard: &lt;code&gt;SECRET_KEY&lt;/code&gt;, &lt;code&gt;DATABASE_URL&lt;/code&gt; copied from the PostgreSQL instance, &lt;code&gt;DEBUG=False&lt;/code&gt;, &lt;code&gt;ALLOWED_HOSTS&lt;/code&gt; set to the Railway-generated domain, &lt;code&gt;CORS_ALLOWED_ORIGINS&lt;/code&gt; set to a placeholder for now since the Vercel URL isn't known yet, and &lt;code&gt;DJANGO_SETTINGS_MODULE&lt;/code&gt; pointing to the production settings file.&lt;/p&gt;

&lt;p&gt;The first deploy takes a few minutes. Railway shows the build logs in real time, which makes debugging straightforward when something goes wrong.&lt;/p&gt;




&lt;h2&gt;
  
  
  Running Migrations on Production
&lt;/h2&gt;

&lt;p&gt;After the first successful deploy, migrations need to be run on the production database. Railway provides a way to run one-off commands directly from the dashboard, the equivalent of &lt;code&gt;python manage.py migrate&lt;/code&gt; but running against the live PostgreSQL database on Railway's servers.&lt;/p&gt;

&lt;p&gt;This is also where a superuser gets created for the production admin panel. The admin panel isn't for public use, but it's useful for inspecting data and managing the database without writing raw SQL.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Broke and How It Was Fixed
&lt;/h2&gt;

&lt;p&gt;Three things broke during deployment.&lt;/p&gt;

&lt;p&gt;The first was a missing &lt;code&gt;DJANGO_SETTINGS_MODULE&lt;/code&gt; environment variable. Railway was using Django's default settings discovery, which found the base settings file instead of production. The settings module path wasn't being passed correctly. Explicitly setting &lt;code&gt;DJANGO_SETTINGS_MODULE=devcollab.settings.production&lt;/code&gt; in Railway's environment variables fixed it immediately.&lt;/p&gt;

&lt;p&gt;The second was &lt;code&gt;ALLOWED_HOSTS&lt;/code&gt;. The Railway-generated domain for the Django service looked different from what I expected; it includes a subdomain and the &lt;code&gt;.railway.app&lt;/code&gt; suffix. The &lt;code&gt;ALLOWED_HOSTS&lt;/code&gt; environment variable needed to include the exact domain Railway assigned, not a guess at what it would be.&lt;/p&gt;

&lt;p&gt;The third was &lt;code&gt;collectstatic&lt;/code&gt; failing because the &lt;code&gt;STATIC_ROOT&lt;/code&gt; directory didn't exist. Railway's build environment doesn't pre-create directories. Adding &lt;code&gt;STATIC_ROOT = BASE_DIR / 'staticfiles'&lt;/code&gt; to the production settings and running &lt;code&gt;mkdir -p staticfiles&lt;/code&gt; in the build command fixed it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing the Live API
&lt;/h2&gt;

&lt;p&gt;With the backend deployed, the entire API was tested in Postman against the live Railway URL, not localhost. Every endpoint, in the same order as the original backend testing: register, login, token refresh, logout, profile, projects CRUD, collaboration requests.&lt;/p&gt;

&lt;p&gt;The live URL test caught one thing that local testing had missed: media file URLs. Profile avatars uploaded locally had URLs pointing to &lt;code&gt;localhost:8000/media/&lt;/code&gt;, fine for development. In production, the &lt;code&gt;MEDIA_URL&lt;/code&gt; needs to point to wherever media files are actually served. For now, media files are stored on Railway's filesystem, which gets wiped on each deploy. This is a known limitation: a proper production app would use something like AWS S3 or Cloudinary. For portfolio demonstration purposes, it's acceptable, and the profile still works if no avatar is uploaded.&lt;/p&gt;

&lt;p&gt;Everything else passed. Authentication, project CRUD with permissions enforced correctly, collaboration requests with the right 400 and 403 responses on invalid operations. The live API behaves identically to the local one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Things Stand After Day 97
&lt;/h2&gt;

&lt;p&gt;The Django backend is live on Railway. PostgreSQL is running. All migrations are applied. Every endpoint is tested and working. The live URL is ready for the Next.js frontend to connect to.&lt;/p&gt;

&lt;p&gt;Thanks for reading. Feel free to share your thoughts!&lt;/p&gt;

</description>
      <category>backend</category>
      <category>devjournal</category>
      <category>devops</category>
      <category>python</category>
    </item>
    <item>
      <title>Day 96 of #100DaysOfCode — DevCollab: Responsive Design and End-to-End Testing</title>
      <dc:creator>M Saad Ahmad</dc:creator>
      <pubDate>Sun, 10 May 2026 10:02:04 +0000</pubDate>
      <link>https://forem.com/m_saad_ahmad/day-96-of-100daysofcode-devcollab-responsive-design-and-end-to-end-testing-4ml6</link>
      <guid>https://forem.com/m_saad_ahmad/day-96-of-100daysofcode-devcollab-responsive-design-and-end-to-end-testing-4ml6</guid>
      <description>&lt;p&gt;Two days until deployment. Today was the last day of building work, making the app work on mobile, and running every user flow from start to finish to find and fix anything broken before it goes live. Not glamorous work, but necessary work. The things found during testing today would have been embarrassing to discover after deploying.&lt;/p&gt;




&lt;h2&gt;
  
  
  Responsive Design
&lt;/h2&gt;

&lt;p&gt;The app was built desktop-first. Every layout decision: two-column grids, side-by-side forms, horizontal navigation, was made with a 1200px viewport in mind. Opening the browser dev tools and switching to a 375px mobile viewport revealed exactly what I expected: broken layouts, overflowing text, buttons too small to tap, and a Navbar that disappeared off the right side of the screen.&lt;/p&gt;

&lt;p&gt;Tailwind's responsive prefixes make fixing this systematic rather than guesswork. Going page by page and applying &lt;code&gt;sm:&lt;/code&gt;, &lt;code&gt;md:&lt;/code&gt;, and &lt;code&gt;lg:&lt;/code&gt; prefixes to the relevant classes turned the desktop layout into a responsive one. The pattern is almost always the same: a two-column grid becomes a single column, a side-by-side form becomes stacked fields, a horizontal stat row becomes a two-by-two grid, and horizontal button groups become full-width stacked buttons.&lt;/p&gt;

&lt;p&gt;The pages that needed the most work were the project detail page, the two-column main content and sidebar layout, the dashboard, the four-column stats row, and the project rows with action buttons on the right. Both collapsed to a single column on mobile, with the action buttons moving below the content they relate to.&lt;/p&gt;

&lt;p&gt;The browse page search bar was the trickiest. Three inputs and two buttons in a horizontal row on a desktop becomes a mess on mobile. The solution was stacking them vertically on small screens, with each input taking full width. The filter button becomes full-width, too. Functionally identical, just arranged differently.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Navbar Mobile Menu
&lt;/h2&gt;

&lt;p&gt;The desktop Navbar has five or six items, depending on the auth state. On a 375px screen, that's an immediate overflow. I added a hamburger menu button that only shows on small screens and toggles a dropdown panel below the Navbar containing all the same links.&lt;/p&gt;

&lt;p&gt;The hamburger button uses Tailwind's &lt;code&gt;md:hidden&lt;/code&gt; to hide on medium and larger screens. The full navigation links use &lt;code&gt;hidden md:flex&lt;/code&gt; to show only on medium and larger. On mobile, the dropdown panel slides down from the Navbar when the hamburger is clicked and closes when any link is selected.&lt;/p&gt;

&lt;p&gt;The dropdown has the same background and border as the Navbar and sits on top of the page content with a high z-index. Each link in the dropdown takes the full width and has enough vertical padding to be easily tappable with a finger.&lt;/p&gt;




&lt;h2&gt;
  
  
  End-to-End Testing
&lt;/h2&gt;

&lt;p&gt;With responsive design done, I ran through every user flow in the browser, not with Postman, not in the admin panel, just the browser from the perspective of a real user. This is the most valuable testing you can do before deployment because it catches the class of bugs that only appear when the full stack is connected, and a real person is navigating through it.&lt;/p&gt;

&lt;p&gt;The flows I tested in order:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;New user registration and onboarding.&lt;/strong&gt; Register with a new username and email, get redirected to the dashboard, see the empty state, click through to edit the profile, fill in skills and bio, save, and see the public profile correctly display the new data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Posting a project.&lt;/strong&gt; Create a new project with all fields filled in, get redirected to the detail page, confirm the tech stack and roles render as tags, confirm the Edit and Delete buttons show since this is the owner's project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Browsing and applying.&lt;/strong&gt; Log out. Log in as a different user. Browse projects. Use the search bar to filter. Click into a project. Confirm that the Request to Collaborate button shows. Click it, write a message, submit. Get redirected to the detail page, confirm that the pending badge shows. Navigate to sent requests and confirm the request appears with a pending status.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Request management.&lt;/strong&gt; Switch back to the project owner account. Open the dashboard. Confirm the incoming request appears. Accept it. Confirm the status badge updates are in place without a page reload. Switch back to the requester account, check sent requests, and confirm the status shows accepted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Permission testing.&lt;/strong&gt; While logged out, try navigating directly to &lt;code&gt;/dashboard&lt;/code&gt;, confirm redirect to login. Try navigating to &lt;code&gt;/projects/new&lt;/code&gt;, confirm redirect. Log in as a user who doesn't own a project and try navigating directly to &lt;code&gt;/projects/[id]/edit&lt;/code&gt;, confirm redirect back to the detail page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Profile and public pages.&lt;/strong&gt; Visit another user's public profile by username. Confirm projects show. Confirm that no edit button is visible since it's not your profile. Try a username that doesn't exist and confirm the not-found page shows.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bugs Found and Fixed
&lt;/h2&gt;

&lt;p&gt;Testing always finds things. Today found five.&lt;/p&gt;

&lt;p&gt;The first was on the apply page. If a user navigated directly to &lt;code&gt;/projects/[id]/apply&lt;/code&gt; for a project that didn't exist, the page threw a JavaScript error because it tried to access properties on a null project object before the not-found check ran. The fix was adding a loading guard so the form only renders once the project data exists.&lt;/p&gt;

&lt;p&gt;The second was on the dashboard. When all incoming requests were accepted or rejected, and none were pending, the requests section showed nothing, not even a message. The empty state component wasn't being rendered in the right condition. Fixed by adjusting the conditional check.&lt;/p&gt;

&lt;p&gt;The third was the edit project form, not initialising the &lt;code&gt;is_open&lt;/code&gt; toggle correctly. The form was treating the boolean &lt;code&gt;false&lt;/code&gt; from the API as a missing value and defaulting to &lt;code&gt;true&lt;/code&gt;, making every project appear open when editing. Fixed by using &lt;code&gt;?? true&lt;/code&gt; instead of &lt;code&gt;|| true&lt;/code&gt;, the nullish coalescing operator correctly handles &lt;code&gt;false&lt;/code&gt;, whereas logical OR converts it to the default.&lt;/p&gt;

&lt;p&gt;The fourth was a CORS issue discovered when testing with the actual Django server running. The Next.js dev server runs on port 3000, but I'd only added &lt;code&gt;http://localhost:3000&lt;/code&gt; to &lt;code&gt;CORS_ALLOWED_ORIGINS&lt;/code&gt;. Adding the variant without a trailing slash fixed it.&lt;/p&gt;

&lt;p&gt;The fifth was the Navbar not updating its display immediately after logout. The user state was being cleared, but the Navbar component wasn't re-rendering. Fixed by ensuring the logout function updates the auth context state synchronously before the redirect.&lt;/p&gt;




&lt;h2&gt;
  
  
  Django Deployment Checklist
&lt;/h2&gt;

&lt;p&gt;With the frontend testing done, I switched to the backend and ran &lt;code&gt;python manage.py check --deploy&lt;/code&gt;. It flagged four things.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;DEBUG = True&lt;/code&gt; — expected, will be set to False in the production environment.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SECRET_KEY&lt;/code&gt; too predictable — the key in &lt;code&gt;.env&lt;/code&gt; for local development was too short. Generated a proper 50-character random key for production.&lt;/p&gt;

&lt;p&gt;Missing &lt;code&gt;SECURE_SSL_REDIRECT&lt;/code&gt; — will be set in production settings.&lt;/p&gt;

&lt;p&gt;Missing &lt;code&gt;SESSION_COOKIE_SECURE&lt;/code&gt; and &lt;code&gt;CSRF_COOKIE_SECURE&lt;/code&gt; — both will be set in production settings.&lt;/p&gt;

&lt;p&gt;None of these are surprises. All of them are standard settings that are intentionally different in development and production. The production settings file — which reads from environment variables — will have all of these set correctly when deployed to Railway.&lt;/p&gt;

&lt;p&gt;WhiteNoise was installed and configured to serve Django's static files in production. Without it, Django requires a separate web server to serve static files — WhiteNoise handles them directly from the Django process, which is simpler for a deployment this size.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Things Stand After Day 96
&lt;/h2&gt;

&lt;p&gt;The app works on mobile. Every user flow passes end-to-end testing. Five bugs were found and fixed. The Django backend passes the deployment check with known production settings flagged. WhiteNoise is configured.&lt;/p&gt;

&lt;p&gt;Thanks for reading. Feel free to share your thoughts!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>nextjs</category>
      <category>100daysofcode</category>
    </item>
    <item>
      <title>Day 95 of #100DaysOfCode — DevCollab: UI Polish, Error Handling, and Loading States</title>
      <dc:creator>M Saad Ahmad</dc:creator>
      <pubDate>Sat, 09 May 2026 09:26:03 +0000</pubDate>
      <link>https://forem.com/m_saad_ahmad/day-95-of-100daysofcode-devcollab-ui-polish-error-handling-and-loading-states-67</link>
      <guid>https://forem.com/m_saad_ahmad/day-95-of-100daysofcode-devcollab-ui-polish-error-handling-and-loading-states-67</guid>
      <description>&lt;p&gt;All the pages are built. Everything works. But "works" and "feels good to use" are two different things. Today was about closing that gap, consistent styling, proper error handling, skeleton loading states, empty states that guide users, and toast notifications that replace raw alerts. No new features. Just making what exists feel like a real product.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Polish Day Matters
&lt;/h2&gt;

&lt;p&gt;It's tempting to skip this day and go straight to deployment. The app functions, the data flows correctly, and the permissions are enforced. What's the point of spending a whole day on loading spinners and empty state messages?&lt;/p&gt;

&lt;p&gt;The point is that a portfolio project isn't just evaluated on whether it works; it's evaluated on whether it feels like something a real team would ship. Raw API error text appearing in the UI, pages that flash blank while loading, empty tables with no explanation, these are the things that make a project feel unfinished, even when the underlying code is solid. One focused polish day removes all of those.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Toast Notification System
&lt;/h2&gt;

&lt;p&gt;The single biggest improvement today was replacing scattered &lt;code&gt;alert()&lt;/code&gt; calls and inline error message strings with a unified toast notification system.&lt;/p&gt;

&lt;p&gt;Before today, error handling was inconsistent. Some pages showed errors in a red div above the form. Some pages used &lt;code&gt;window.alert()&lt;/code&gt;. Some pages silently failed. A user accepting a collaboration request on the dashboard had no feedback confirming it worked; the button just changed state with no message.&lt;/p&gt;

&lt;p&gt;The toast system fixes all of this. It's a context-based notification system: a &lt;code&gt;ToastContext&lt;/code&gt; that any component can call to show a success, error, or info notification. The toast appears in the bottom-right corner of the screen, stays visible for three seconds, and then fades out. Multiple toasts can stack. Each has an appropriate color: green for success, red for error, and blue for info.&lt;/p&gt;

&lt;p&gt;Every action that previously had inconsistent feedback now calls the toast system. Project created; green toast. Request sent; green toast. Request accepted; green toast. API call failed; red toast with a readable message extracted from the Django error response. The user always knows what happened.&lt;/p&gt;




&lt;h2&gt;
  
  
  Skeleton Loading States
&lt;/h2&gt;

&lt;p&gt;The previous loading implementation was a centered spinner on every page. It worked, but it was jarring; the page would be blank, then a spinner, then suddenly full of content. The content shift felt abrupt.&lt;/p&gt;

&lt;p&gt;Skeleton screens solve this by showing placeholder shapes in the layout that the content will occupy. The browse projects page now shows three skeleton project cards during load, grey rectangles in the same grid as real cards, with the same card dimensions, borders, and rounded corners. When the real data arrives, the skeletons are replaced. The layout doesn't shift because the placeholders already occupy the same space.&lt;/p&gt;

&lt;p&gt;The dashboard shows skeleton rows for the projects section and skeleton request cards for the incoming requests section. The profile page shows a skeleton avatar circle, skeleton text lines for the bio and skills, and skeleton project cards below. Each skeleton matches the shape of its real content, so the transition from loading to loaded feels smooth rather than abrupt.&lt;/p&gt;




&lt;h2&gt;
  
  
  Empty States with Guidance
&lt;/h2&gt;

&lt;p&gt;Every page that can legitimately have zero items now has a proper empty state. The key insight about empty states is that they're not just informational, they're navigational. A user landing on an empty page needs to know what to do next, not just that nothing is there yet.&lt;/p&gt;

&lt;p&gt;The browse projects page, when no projects exist, or no results match the search, shows a message explaining the situation and either a "Clear filters" link or a "Post the first project" link, depending on whether filters are active.&lt;/p&gt;

&lt;p&gt;The dashboard's projects section, for a new user who hasn't posted anything yet, shows an illustration space, the message "You haven't posted any projects yet", and a prominent "Post Your First Project" button. The dashboard's requests section, when no requests have come in, shows "No collaboration requests yet" with a softer message explaining that requests will appear here when developers apply to their projects.&lt;/p&gt;

&lt;p&gt;The sent requests page for a new user shows "You haven't applied to any projects yet" with a link to browse projects.&lt;/p&gt;

&lt;p&gt;The profile page, when viewed as a logged-in user who hasn't set up their profile, shows a subtle prompt to complete the profile: "Your profile is looking a bit empty. Add your skills and bio so other developers know what you can bring to a project."&lt;/p&gt;




&lt;h2&gt;
  
  
  Consistent Styling Pass
&lt;/h2&gt;

&lt;p&gt;Going through every page on the same day revealed a lot of small inconsistencies that I hadn't noticed when building them on different days. The create project page used slightly different button styling than the edit project page. The browse page cards had different padding than the dashboard project rows. Some pages had a max-width container, some didn't.&lt;/p&gt;

&lt;p&gt;I went through each page and applied a consistent set of Tailwind patterns. All cards have the same padding, border, shadow, and hover transition. All primary buttons are the same blue with the same rounded corners. All headings at the same level use the same font size and weight. All form fields look identical across all forms, with the same border, same focus ring, and same error state styling.&lt;/p&gt;

&lt;p&gt;This kind of pass is only possible efficiently when all the pages exist at the same time. Building them one at a time means you can't see the inconsistencies until you view the whole app together.&lt;/p&gt;




&lt;h2&gt;
  
  
  Error Handling Improvements
&lt;/h2&gt;

&lt;p&gt;Raw API error responses are not user-friendly. Django returns errors in formats like &lt;code&gt;{"title": ["This field may not be blank."]}&lt;/code&gt; or &lt;code&gt;{"detail": "Authentication credentials were not provided."}&lt;/code&gt;. Before today, these were sometimes shown directly to users.&lt;/p&gt;

&lt;p&gt;I wrote a small helper function: &lt;code&gt;parseApiError&lt;/code&gt;, that takes an error response from the API and returns a readable string. It handles the most common Django error formats: field-specific errors as an object, a detail string, a non-field errors array, and a plain error string. The output is always a clean sentence that the user can read.&lt;/p&gt;

&lt;p&gt;Every catch block in every service call now runs the error through &lt;code&gt;parseApiError&lt;/code&gt; before showing it in a toast or form error state. No more raw JSON visible anywhere in the UI.&lt;/p&gt;




&lt;h2&gt;
  
  
  Form Feedback Improvements
&lt;/h2&gt;

&lt;p&gt;Three specific improvements to forms across the app.&lt;/p&gt;

&lt;p&gt;First, submit buttons are consistently disabled while a form is submitting. Before today, some forms disabled their button during submission, and some didn't. Consistent behavior means users can't accidentally double-submit a form on a slow connection.&lt;/p&gt;

&lt;p&gt;Second, form field validation errors are visually clearer. The red error text is now positioned immediately below the relevant field with a red border on the input, not just red text somewhere on the page.&lt;/p&gt;

&lt;p&gt;Third, the edit profile and edit project forms now show a subtle "No changes made" message if the user submits the form without changing anything. This is detected by comparing the current form values to the initial values. If nothing changed, there's no point in making an API call.&lt;/p&gt;




&lt;h2&gt;
  
  
  Navbar Active State
&lt;/h2&gt;

&lt;p&gt;A small detail that makes navigation feel more intentional: the current page's link in the Navbar is highlighted. The &lt;code&gt;usePathname&lt;/code&gt; hook from Next.js returns the current URL path. The Navbar compares each link's target path to the current pathname and applies an active style, a slightly different text color, and a subtle underline to the matching link.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Things Stand After Day 95
&lt;/h2&gt;

&lt;p&gt;The app looks consistent. Errors are friendly. Loading states are smooth. Empty states guide users toward action. Forms give clear feedback. The entire UI has been reviewed in one session, and inconsistencies are resolved.&lt;/p&gt;

&lt;p&gt;Thanks for reading. Feel free to share your thoughts!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>nextjs</category>
      <category>100daysofcode</category>
    </item>
    <item>
      <title>Day 94 of #100DaysOfCode — DevCollab: Dashboard, Profiles, and the Final Pages</title>
      <dc:creator>M Saad Ahmad</dc:creator>
      <pubDate>Fri, 08 May 2026 11:07:13 +0000</pubDate>
      <link>https://forem.com/m_saad_ahmad/day-94-of-100daysofcode-devcollab-dashboard-profiles-and-the-final-pages-2d22</link>
      <guid>https://forem.com/m_saad_ahmad/day-94-of-100daysofcode-devcollab-dashboard-profiles-and-the-final-pages-2d22</guid>
      <description>&lt;p&gt;Four days of backend. Four days of frontend building. Today was the last building day: the dashboard, public profiles, edit profile, and sent requests. After today, every planned page exists. The entire app is navigable directly in the browser without using Postman or the admin panel.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Dashboard
&lt;/h2&gt;

&lt;p&gt;The dashboard is the page users land on after logging in, and it needed to answer one question clearly: What's happening with my projects right now?&lt;/p&gt;

&lt;p&gt;I split it into two sections. The top section shows the user's own projects; a list of cards with the project title, status badge, whether it's open or closed, and two action links: one to view incoming requests for that project, one to edit it. Below each project card is the count of pending requests, shown as a link. Clicking it scrolls to or filters the requests section below.&lt;/p&gt;

&lt;p&gt;The second section shows all incoming collaboration requests across all the user's projects grouped by project. Each request shows the requester's username, their skills as small tags, a preview of their message, and two buttons: accept and reject. Clicking either calls the update request status API, updates the UI immediately without a full page reload, and shows the new status. I wanted this interaction to feel instant, not a redirect, just the button state changing and the badge updating in place.&lt;/p&gt;

&lt;p&gt;The stats row at the top, total projects, total requests received, and accepted collaborators, gives a quick snapshot without having to count manually. These numbers are computed from the data already fetched for the two sections, not from separate API calls.&lt;/p&gt;

&lt;p&gt;The dashboard is a protected page. Unauthenticated users get redirected to the login.&lt;/p&gt;




&lt;h2&gt;
  
  
  The RequestCard Component
&lt;/h2&gt;

&lt;p&gt;The dashboard's request section uses a &lt;code&gt;RequestCard&lt;/code&gt; component that I extracted because request cards appear both on the dashboard and potentially on individual project pages. The card shows the requester's avatar if they have one, their username as a link to their public profile, their skills as tags, the project title it belongs to, the message they wrote, and the accept and reject buttons.&lt;/p&gt;

&lt;p&gt;The accept and reject buttons have three visual states: default, loading while the API call is in flight, and a disabled state after one has been clicked. Once a request is accepted or rejected, the buttons disappear, and a status badge takes their place. This prevents the owner from changing their mind through the UI if they want to change a status, they'd have to use the API directly, which is fine for now.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Public Profile Page
&lt;/h2&gt;

&lt;p&gt;The public profile page is accessible to anyone logged in or not. It shows everything a developer has chosen to share about themselves: their avatar, username, bio, location, skills as tags, GitHub link, LinkedIn link, and website link. Below the profile info is a grid of their active open projects.&lt;/p&gt;

&lt;p&gt;The projects section on a profile page reuses the &lt;code&gt;ProjectCard&lt;/code&gt; component from day 92. No new component is needed; the same card that appears on the browse page appears here. That reuse is the payoff for building &lt;code&gt;ProjectCard&lt;/code&gt; as a standalone component on day 92 rather than inline on the browse page.&lt;/p&gt;

&lt;p&gt;The page handles the case where the username in the URL doesn't exist, the API returns a 404, and the page shows a clean "User not found" message with a link back to the browse page.&lt;/p&gt;

&lt;p&gt;One intentional design decision: the profile page doesn't show the user's sent collaboration requests or any private information. Only what the user has explicitly put in their public profile. The skills and bio are the user's own words; the projects are already public. Nothing private surfaces here.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Edit Profile Page
&lt;/h2&gt;

&lt;p&gt;The edit profile page is where users set up their presence on DevCollab. First impressions on the platform come from this page; a well-filled profile gets more collaboration requests.&lt;/p&gt;

&lt;p&gt;The form has seven fields: bio, skills, location, GitHub URL, LinkedIn URL, website URL, and avatar upload. The skills field is a plain text input with a hint showing the comma-separated format. On submit, the page calls the update profile API with a PATCH request.&lt;/p&gt;

&lt;p&gt;Avatar upload required special handling. The API endpoint accepts multipart form data for avatar uploads, not JSON. The profile update with text fields uses JSON. I handled this by separating the two text fields update via PATCH with JSON, and avatar updates via a separate POST to the avatar endpoint with FormData. The user sees one form, but under the hood, the two updates happen separately if an avatar is selected.&lt;/p&gt;

&lt;p&gt;The form pre-fills with the current user's profile data fetched on mount. Fields the user hasn't filled in yet show as empty. On successful save, the page redirects to the user's public profile so they can see exactly what others will see.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Sent Requests Page
&lt;/h2&gt;

&lt;p&gt;The sent requests page is the requester's mirror of the dashboard. Where the dashboard shows incoming requests to the owner, this page shows outgoing requests from the requester.&lt;/p&gt;

&lt;p&gt;Each row shows the project title as a link to the detail page, the project owner's username, the date the request was sent, the message the user wrote, and the current status as a colored badge: grey for pending, blue for reviewed, green for accepted, red for rejected.&lt;/p&gt;

&lt;p&gt;The page is sorted by most recent first. Old rejected requests stay visible; removing them would hide useful information. A developer who got rejected from a project might want to know that when they're evaluating whether to apply to something similar.&lt;/p&gt;

&lt;p&gt;The empty state on this page is important. A new user who has registered but hasn't applied anywhere yet lands here and sees "You haven't sent any collaboration requests yet" with a link to browse projects. It guides them toward the next action rather than showing a blank table.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Users Service
&lt;/h2&gt;

&lt;p&gt;All four pages today use profile data from the API. Rather than making direct API calls from each page, I wrote &lt;code&gt;services/users.js&lt;/code&gt; first, a dedicated file with functions for getting the current user's profile, updating it, uploading an avatar, and fetching any public profile by username. The same principle as the projects and request services, API logic in service files, and pages focused on rendering.&lt;/p&gt;




&lt;h2&gt;
  
  
  Completing the Navigation
&lt;/h2&gt;

&lt;p&gt;With all pages existing today, I also finished wiring up the navigation links that were previously pointing to pages that didn't exist yet.&lt;/p&gt;

&lt;p&gt;The Navbar's Dashboard link now actually leads somewhere meaningful. The profile username in the Navbar links to the current user's public profile. The "Post Project" button in the Navbar links to the create project page. Every link in the app now resolves to a real page.&lt;/p&gt;

&lt;p&gt;I also added a breadcrumb-style back link on the edit profile, edit project, and apply pages so users can navigate back without using the browser back button. Small detail, noticeably better experience.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Things Stand After Day 94
&lt;/h2&gt;

&lt;p&gt;Every page is built. Every user flow works end-to-end through the browser. The complete journey register, fill in profile, post a project, another user browses and applies, the owner accepts from their dashboard, both sides see the updated status, works without touching anything outside the browser.&lt;/p&gt;

&lt;p&gt;What's missing is polish. The pages work, but they don't all look their best. Some loading states are basic, some empty states are minimal, and some error messages are raw API text rather than friendly messages. That's what the next two days are for.&lt;/p&gt;

&lt;p&gt;Thanks for reading. Feel free to share your thoughts!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>sideprojects</category>
      <category>100daysofcode</category>
    </item>
    <item>
      <title>Day 93 of #100DaysOfCode — DevCollab: Create, Edit, Delete Projects + Collaboration Requests</title>
      <dc:creator>M Saad Ahmad</dc:creator>
      <pubDate>Thu, 07 May 2026 08:54:57 +0000</pubDate>
      <link>https://forem.com/m_saad_ahmad/day-93-of-100daysofcode-devcollab-create-edit-delete-projects-collaboration-requests-1g6a</link>
      <guid>https://forem.com/m_saad_ahmad/day-93-of-100daysofcode-devcollab-create-edit-delete-projects-collaboration-requests-1g6a</guid>
      <description>&lt;p&gt;Yesterday was read-only; browsing and viewing. Today was the write side. Users can now create projects, edit their own, delete them, and send collaboration requests. The platform went from a read-only directory to an interactive two-sided system in one day of building.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Reusable ProjectForm Component
&lt;/h2&gt;

&lt;p&gt;Before building the create and edit pages, I built &lt;code&gt;ProjectForm&lt;/code&gt;, a single form component used by both. The create page uses it empty. The edit page uses it pre-filled with existing project data. Writing the form once and reusing it in two places means any change to the form, adding a field, changing validation, or updating styling, happens in one file, and both pages get it automatically.&lt;/p&gt;

&lt;p&gt;The form has six fields: title, description, tech stack, roles needed, a status dropdown, and an is_open toggle. Tech stack and roles needed are plain text inputs where the user types comma-separated values, the same format the Django API expects and stores. Simple, no tag input library needed.&lt;/p&gt;

&lt;p&gt;The form component accepts two props: &lt;code&gt;initialValues&lt;/code&gt; for pre-filling in the edit case, and &lt;code&gt;onSubmit&lt;/code&gt;, which is called with the form data when the user submits. The component handles its own field state and validation, checking required fields before calling &lt;code&gt;onSubmit&lt;/code&gt;. Error messages appear below each field that fails validation.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;onSubmit&lt;/code&gt; prop pattern is the key architectural decision here. The form component doesn't know whether it's creating or editing. It just collects data and hands it to whoever is using it. The create page passes a function that calls the create API. The edit page passes a function that calls the update API. Same form, different behavior depending on which page uses it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Create Project Page
&lt;/h2&gt;

&lt;p&gt;The create project page is a protected page, &lt;code&gt;ProtectedRoute&lt;/code&gt; wraps it, so unauthenticated users get redirected to login. It renders the &lt;code&gt;ProjectForm&lt;/code&gt; with no initial values and an &lt;code&gt;onSubmit&lt;/code&gt; handler that calls &lt;code&gt;createProject&lt;/code&gt; from the projects service.&lt;/p&gt;

&lt;p&gt;On successful creation, the API returns the new project object, including its ID. The page uses that ID to redirect to &lt;code&gt;/projects/[id]&lt;/code&gt;, the detail page of the just-created project. The user sees their new project immediately after creating it, which provides clear confirmation that the action worked.&lt;/p&gt;

&lt;p&gt;Error handling on the create page covers two cases: network errors and validation errors from Django. If Django returns a 400 with field-specific errors. For example, if the title is too long, those errors are passed back to the form component and displayed under the relevant fields. If it's a general network error, a top-level error message is shown.&lt;/p&gt;




&lt;h2&gt;
  
  
  Edit Project Page
&lt;/h2&gt;

&lt;p&gt;The edit page is more complex than the create page because it needs to fetch the existing project before rendering the form. It also needs to verify that the current user is actually the owner. If someone navigates directly to &lt;code&gt;/projects/[id]/edit&lt;/code&gt; and they're not the owner, they should be redirected away rather than seeing a form they can't legitimately use.&lt;/p&gt;

&lt;p&gt;The page fetches the project on the mount. If the fetch fails or the project doesn't exist, it shows an error. If the project exists but the current user is not the owner, compared by username since that's what the API returns, the page redirects to the project's detail page.&lt;/p&gt;

&lt;p&gt;Once the ownership check passes, the form renders pre-filled with the existing project data. The &lt;code&gt;initialValues&lt;/code&gt; prop receives the project fields mapped to the form field names. On submission, the page calls &lt;code&gt;updateProject&lt;/code&gt; with a PATCH request, only sending the fields that were actually changed. The Django API handles partial updates correctly because the endpoint accepts PATCH.&lt;/p&gt;

&lt;p&gt;On success, the page redirects to the project detail page. The detail page immediately shows the updated data because it fetches fresh data on mount.&lt;/p&gt;




&lt;h2&gt;
  
  
  Delete Project
&lt;/h2&gt;

&lt;p&gt;Delete was partially wired up on the detail page yesterday; the button existed, but the flow wasn't complete. Today I finished it properly.&lt;/p&gt;

&lt;p&gt;The delete flow uses a confirmation dialog, &lt;code&gt;window.confirm&lt;/code&gt; for now, a proper modal in the polish phase. If the user confirms, the page calls &lt;code&gt;deleteProject&lt;/code&gt;, shows a loading state on the button, and redirects to &lt;code&gt;/dashboard&lt;/code&gt; on success. If the delete fails, the error is displayed, and the button returns to its normal state.&lt;/p&gt;

&lt;p&gt;The redirect goes to the dashboard rather than the project list because the deleted project no longer exists. Redirecting to the list would show the list without the deleted project, which is fine, but the dashboard is more useful; the user can see their remaining projects immediately.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Collaboration Request Flow
&lt;/h2&gt;

&lt;p&gt;The apply page is the final piece of the user journey that makes DevCollab a platform rather than just a directory. A developer finds a project they want to join, clicks "Request to Collaborate", writes a message, and submits.&lt;/p&gt;

&lt;p&gt;The apply page is protected. It fetches the project on the mount to display the project summary alongside the form, so the user can see what they're applying for while writing their message. This context matters: a message written knowing the project details is more relevant than one written blindly.&lt;/p&gt;

&lt;p&gt;The form has a single field message with a character minimum. An empty or one-line message shouldn't be submittable because it's not useful to the project owner. The character minimum encourages genuine applications.&lt;/p&gt;

&lt;p&gt;On submission, the page calls &lt;code&gt;sendRequest&lt;/code&gt; from the requests service. Two error cases are handled explicitly. If the API returns a 400 saying the user has already sent a request, which can happen if someone navigates directly to the apply URL after already applying, the page shows a message and redirects back to the detail page. If the project is not accepting requests, &lt;code&gt;is_open&lt;/code&gt; is false, the apply page redirects back to the detail page with a message.&lt;/p&gt;

&lt;p&gt;On success, the page redirects to the project detail page. The detail page now shows the request status badge 'pending' because the &lt;code&gt;request_status&lt;/code&gt; field in the API response reflects the newly created request. The "Request to Collaborate" button is gone, replaced by the pending badge. The user gets immediate visual confirmation that their request was sent.&lt;/p&gt;




&lt;h2&gt;
  
  
  Connecting the Write and Read Sides
&lt;/h2&gt;

&lt;p&gt;Today's work closed the loop between the two sides of the platform. The complete user journey now works end to end:&lt;/p&gt;

&lt;p&gt;User A registers, completes their profile, and creates a project. The project appears on the browse page. User B registers, browses, finds the project, reads the detail page, clicks apply, writes a message, and submits. User B's detail page now shows "Request Pending". User A goes to their dashboard and sees the incoming request. User A goes to Postman or tomorrow, the dashboard to accept or reject.&lt;/p&gt;

&lt;p&gt;The only piece missing is the frontend for request management on the owner's side, that's the dashboard.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Things Stand After Day 93
&lt;/h2&gt;

&lt;p&gt;The write side of the frontend is complete. Create, edit, delete, and apply all work with real data. The reusable &lt;code&gt;ProjectForm&lt;/code&gt; keeps the form logic in one place. The apply flow handles edge cases, duplicate requests, and closed projects gracefully.&lt;/p&gt;

&lt;p&gt;Tomorrow: dashboard page showing the user's projects and incoming requests, public profile page, edit profile page, and the sent requests page.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>showdev</category>
      <category>sideprojects</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Day 92 of #100DaysOfCode — DevCollab: Browse Projects and Project Detail Pages</title>
      <dc:creator>M Saad Ahmad</dc:creator>
      <pubDate>Wed, 06 May 2026 10:04:42 +0000</pubDate>
      <link>https://forem.com/m_saad_ahmad/day-92-of-100daysofcode-devcollab-browse-projects-and-project-detail-pages-5ba4</link>
      <guid>https://forem.com/m_saad_ahmad/day-92-of-100daysofcode-devcollab-browse-projects-and-project-detail-pages-5ba4</guid>
      <description>&lt;p&gt;Yesterday, the authentication foundation was built, Axios with JWT interceptors, auth context, login, and register pages. Today was the first day real data from the Django API appeared in the browser. The browse projects page and the project detail page: the two most important public-facing pages in DevCollab.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Projects Service
&lt;/h2&gt;

&lt;p&gt;Before building any page, I wrote &lt;code&gt;services/projects.js&lt;/code&gt;: a dedicated file for all project-related API calls. Every component that needs project data calls a function from this file rather than making Axios calls directly. This keeps API logic in one place and components focused on rendering.&lt;/p&gt;

&lt;p&gt;The service has functions for fetching all projects with optional search and filter parameters, fetching a single project by ID, and fetching the current user's own projects. Search and filter parameters are passed as query params to the Axios instance, which appends them to the URL automatically. The Django API handles the filtering on the server side and returns the right results.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Browse Projects Page
&lt;/h2&gt;

&lt;p&gt;The browse projects page is the homepage of DevCollab for anyone who isn't logged in yet. It's the first thing a visitor sees and the main entry point into the platform.&lt;/p&gt;

&lt;p&gt;The page has two sections: the search and filter bar at the top, and the project card grid below it.&lt;/p&gt;

&lt;p&gt;The search and filter bar has three inputs: a keyword search field, a tech stack filter, and a role filter. When the user types and submits, the values are pushed into the URL as query parameters. The page reads these parameters from the URL, passes them to the project's service function, and displays the filtered results. Using the URL for filter state means the filtered view is shareable and bookmarkable; copying the URL gives you the same filtered results. It also means the browser back button works correctly.&lt;/p&gt;

&lt;p&gt;The project card grid renders a &lt;code&gt;ProjectCard&lt;/code&gt; component for each project returned from the API. If the API returns an empty array, an empty state message is shown with a prompt to clear the filters. While the data is loading, a loading state is shown, skeleton cards, or a spinner, so the page doesn't flash blank content.&lt;/p&gt;

&lt;p&gt;The page is fully public; no authentication is required to browse. The Django API's project list endpoint is open, so no Authorization header is needed for this page.&lt;/p&gt;




&lt;h2&gt;
  
  
  The ProjectCard Component
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ProjectCard&lt;/code&gt; is the most reused component in the app. It appears on the browse page, the dashboard, and the public profile page. Getting it right today means all three pages benefit.&lt;/p&gt;

&lt;p&gt;The card shows five pieces of information: the project title as a link to the detail page, the owner's username, a truncated description, tech stack tags, and roles needed tags. The tech stack and roles are rendered as individual colored badges using the &lt;code&gt;TechStackTag&lt;/code&gt; and &lt;code&gt;RoleTag&lt;/code&gt; components, respectively.&lt;/p&gt;

&lt;p&gt;The design decision on description truncation was deliberate, showing the first 120 characters with an ellipsis. Full descriptions can be long, and showing them in full on the browse page would make cards have inconsistent heights and hard to scan. The detail page shows the full description.&lt;/p&gt;

&lt;p&gt;The card's click target is the title, a Next.js &lt;code&gt;Link&lt;/code&gt; component pointing to &lt;code&gt;/projects/[id]&lt;/code&gt;. The whole card isn't clickable because the owner's username also needs to be a link to the user's profile. Two separate link targets on the same card means that making the whole card clickable would create nested anchors.&lt;/p&gt;




&lt;h2&gt;
  
  
  TechStackTag and RoleTag
&lt;/h2&gt;

&lt;p&gt;Two tiny components that do one thing each render a styled badge for a tech stack item or a role. They're separate components rather than inline styles because they're used in multiple places, and having them as components means styling them in one place updates them everywhere.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;TechStackTag&lt;/code&gt; renders with a blue color scheme. &lt;code&gt;RoleTag&lt;/code&gt; renders with a green color scheme. The visual distinction helps users quickly scan what a project uses versus what roles it needs.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Project Detail Page
&lt;/h2&gt;

&lt;p&gt;The project detail page is the most complex page built so far because it renders differently for four different types of users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unauthenticated users&lt;/strong&gt; see the full project information and a "Login to apply" link where the collaboration button would be. They can read everything but can't take any action.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The project owner&lt;/strong&gt; sees edit and delete buttons instead of a collaboration button. They can't apply it to their own project. Clicking delete shows a confirmation before making the delete API call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authenticated users who have already sent a request&lt;/strong&gt; see a status badge, pending, accepted, or rejected, instead of an apply button. The status comes from the &lt;code&gt;request_status&lt;/code&gt; field in the project detail API response, which the Django serializer computed from the current user's request on that project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authenticated users who haven't applied yet&lt;/strong&gt; see the "Request to Collaborate" button. Clicking it navigates to &lt;code&gt;/projects/[id]/apply&lt;/code&gt;, the application form page being built tomorrow.&lt;/p&gt;

&lt;p&gt;The page layout is a two-column grid on desktop, the main project information on the left taking up two-thirds of the width, and a sidebar on the right showing the owner's profile card and the action button. On mobile, the layout collapses to a single column with the action section moving below the project info.&lt;/p&gt;

&lt;p&gt;The owner profile card in the sidebar shows the owner's avatar, username as a link to their profile, bio, skills tags, and GitHub link if they've set one. All of this data comes from the &lt;code&gt;owner_data&lt;/code&gt; field nested in the project detail API response.&lt;/p&gt;




&lt;h2&gt;
  
  
  URL-Based Filter State
&lt;/h2&gt;

&lt;p&gt;The decision to store search and filter state in the URL rather than in React state deserves explanation because it's a pattern worth understanding.&lt;/p&gt;

&lt;p&gt;The naive approach is to store filter values in &lt;code&gt;useState&lt;/code&gt;. This works but has two problems: refreshing the page loses the filter state, and the filtered URL can't be shared with someone else.&lt;/p&gt;

&lt;p&gt;The better approach is &lt;code&gt;useSearchParams&lt;/code&gt; and &lt;code&gt;useRouter&lt;/code&gt; from Next.js. When a filter is applied, &lt;code&gt;router.push&lt;/code&gt; updates the URL with the new query parameters. The page reads the current filter values from &lt;code&gt;useSearchParams&lt;/code&gt; on every render. When the URL changes, whether from user input or browser navigation, the component re-renders and refetches with the new parameters.&lt;/p&gt;

&lt;p&gt;This means &lt;code&gt;/projects?search=django&amp;amp;tech_stack=react&lt;/code&gt; always shows the same results regardless of how you arrived at that URL. It's a small architectural decision that makes the app feel significantly more polished.&lt;/p&gt;




&lt;h2&gt;
  
  
  Loading and Empty States
&lt;/h2&gt;

&lt;p&gt;Both pages have proper loading and empty states. This is one of those things that's easy to skip during development and embarrassing to ship without.&lt;/p&gt;

&lt;p&gt;The browse page shows three skeleton cards during the initial load, gray placeholder shapes in the same layout as real cards. Once the data arrives, the skeletons are replaced with real cards. If the data fetch fails, an error message with a retry option is shown.&lt;/p&gt;

&lt;p&gt;The empty state on the browse page, when search or filters return no results, shows a message explaining there are no matching projects and offers a "Clear filters" link that resets all filter parameters and returns to the full list.&lt;/p&gt;

&lt;p&gt;The project detail page shows a centered loading spinner while the project data is being fetched. If a project ID in the URL doesn't exist, the API returns 404; the page shows a "Project not found" message with a link back to the browse page. Next.js's &lt;code&gt;notFound()&lt;/code&gt; function handles this cleanly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Things Stand After Day 92
&lt;/h2&gt;

&lt;p&gt;Two complete public pages are working with real data. Users can browse projects, search and filter the results, and view full project details. The correct action, login prompt, owner controls, status badge, or apply button renders based on who is viewing.&lt;/p&gt;

&lt;p&gt;Thanks for reading. Feel free to share your thoughts!&lt;/p&gt;

</description>
      <category>codenewbie</category>
      <category>devjournal</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Day 91 of #100DaysOfCode — DevCollab: Next.js Setup and Authentication</title>
      <dc:creator>M Saad Ahmad</dc:creator>
      <pubDate>Tue, 05 May 2026 09:37:15 +0000</pubDate>
      <link>https://forem.com/m_saad_ahmad/day-91-of-100daysofcode-devcollab-nextjs-setup-and-authentication-4plo</link>
      <guid>https://forem.com/m_saad_ahmad/day-91-of-100daysofcode-devcollab-nextjs-setup-and-authentication-4plo</guid>
      <description>&lt;p&gt;Today, the frontend starts. Day 91 was the most infrastructure-heavy day on the Next.js side, not because of what's visible, but because of what everything else depends on. The Axios instance with JWT interceptors, the auth context, protected routes, and the login and register pages. By the end of today, a user can register in the browser, be redirected to a dashboard, log out, log back in, and have the access token refreshed invisibly in the background when it expires.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting Up the Next.js Project
&lt;/h2&gt;

&lt;p&gt;The setup follows the same principle as the Django backend: get the structure right before writing features. Installing everything at once, organizing folders from day one, and configuring the environment before touching any component.&lt;/p&gt;

&lt;p&gt;Next.js is installed with the App Router: the newer file-based routing system that uses the &lt;code&gt;app/&lt;/code&gt; directory instead of the older &lt;code&gt;pages/&lt;/code&gt; directory. Tailwind CSS is set up during installation. Axios is installed separately for API communication.&lt;/p&gt;

&lt;p&gt;The folder structure inside &lt;code&gt;src/&lt;/code&gt; is organized into five areas: &lt;code&gt;app/&lt;/code&gt; for pages and routing, &lt;code&gt;components/&lt;/code&gt; for reusable UI, &lt;code&gt;context/&lt;/code&gt; for global state, &lt;code&gt;hooks/&lt;/code&gt; for custom hooks, and &lt;code&gt;services/&lt;/code&gt; for API communication functions. This structure is set up on day one because reorganizing imports mid-project is painful and unnecessary.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Axios Instance — The Most Important File in the Frontend
&lt;/h2&gt;

&lt;p&gt;Every API call in the entire Next.js app goes through one Axios instance defined in &lt;code&gt;services/api.js&lt;/code&gt;. This is the architectural decision that makes everything else clean.&lt;/p&gt;

&lt;p&gt;The instance is created with the Django API base URL from an environment variable, &lt;code&gt;NEXT_PUBLIC_API_URL&lt;/code&gt; in &lt;code&gt;.env.local&lt;/code&gt;. This means switching between the local Django server and the deployed Railway URL is a single environment variable change with no code modifications.&lt;/p&gt;

&lt;p&gt;Two interceptors are attached to this instance.&lt;/p&gt;

&lt;p&gt;The request interceptor runs before every outgoing request. It reads the access token from localStorage and attaches it as an Authorization header &lt;code&gt;Bearer &amp;lt;token&amp;gt;&lt;/code&gt;. If no token exists, the request goes out without an Authorization header, which is correct behavior for public endpoints.&lt;/p&gt;

&lt;p&gt;The response interceptor runs after every incoming response. It watches for 401 Unauthorized responses, which means the access token has expired. When it catches one, it automatically posts to the Django token refresh endpoint with the stored refresh token, receives a new access token, stores it in localStorage, updates the Authorization header on the failed request, and retries it. If the refresh also fails, meaning the refresh token has also expired, it clears all tokens from localStorage and redirects to the login page. This entire process is transparent to the user and to every component that makes API calls. No component ever needs to handle token expiry manually.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Auth Service
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;services/auth.js&lt;/code&gt; contains four functions: register, login, logout, and getCurrentUser, that the rest of the frontend calls when it needs to interact with the authentication API.&lt;/p&gt;

&lt;p&gt;Register sends the user's data to the Django register endpoint and stores the returned access and refresh tokens in localStorage along with the user data. Login does the same, but calls the login endpoint. Logout clears all tokens and user data from localStorage. getCurrentUser reads the stored user data from localStorage and returns it. This is how the auth context initializes on page load without needing an API call.&lt;/p&gt;

&lt;p&gt;Keeping these functions in a dedicated service file means the auth logic lives in one place. If the token storage mechanism changes. For example, moving from localStorage to httpOnly cookies, only this file needs updating.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Auth Context
&lt;/h2&gt;

&lt;p&gt;The auth context is what makes the authentication state available to every component in the app without prop drilling. It holds three things: the current user object, a login function, and a logout function.&lt;/p&gt;

&lt;p&gt;On mount, it reads the current user from localStorage via the auth service. This is how the app knows whether a user is logged in when they refresh the page: the token and user data persist in localStorage across browser sessions until the user logs out or the tokens expire.&lt;/p&gt;

&lt;p&gt;The login function takes the credentials, calls the auth service login function, updates the user state in context, and handles the redirect. The logout function calls the auth service logout function, clears the user state, and redirects to the login page.&lt;/p&gt;

&lt;p&gt;The context provider wraps the entire app in &lt;code&gt;layout.js&lt;/code&gt;, the root layout file. This means every page and component in the app has access to the auth state through the &lt;code&gt;useAuth&lt;/code&gt; hook.&lt;/p&gt;




&lt;h2&gt;
  
  
  The useAuth Hook
&lt;/h2&gt;

&lt;p&gt;A single-line custom hook that wraps &lt;code&gt;useContext(AuthContext)&lt;/code&gt;. Every component that needs the current user or the login/logout functions calls &lt;code&gt;useAuth()&lt;/code&gt; rather than importing and using &lt;code&gt;useContext&lt;/code&gt; directly. This is a small abstraction, but it makes components cleaner and easier to read.&lt;/p&gt;




&lt;h2&gt;
  
  
  Protected Routes
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;ProtectedRoute&lt;/code&gt; component wraps every page that requires authentication. It checks for a token in localStorage on mount. If no token exists, it redirects to &lt;code&gt;/login&lt;/code&gt; immediately, before the page renders. If a token exists, it renders the children.&lt;/p&gt;

&lt;p&gt;The redirect includes the intended URL as a query parameter, so after the user logs in, they return to where they were trying to go. This is the same &lt;code&gt;?next=&lt;/code&gt; pattern from Django's login_required decorator, implemented on the frontend.&lt;/p&gt;

&lt;p&gt;In the App Router, protected pages import &lt;code&gt;ProtectedRoute&lt;/code&gt; and wrap their content with it. It's a straightforward pattern, one import and one wrapper component per protected page.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Navbar
&lt;/h2&gt;

&lt;p&gt;The Navbar component reads the current user from the auth context and renders different content based on the auth state. Unauthenticated users see links to the landing page, projects browse page, login, and register. Authenticated users see links to the dashboard, create a project, their profile, and a logout button.&lt;/p&gt;

&lt;p&gt;The Navbar is included in the root layout, so it appears on every page automatically. The landing page and auth pages show the minimal version; all other pages show the full authenticated version.&lt;/p&gt;




&lt;h2&gt;
  
  
  Login and Register Pages
&lt;/h2&gt;

&lt;p&gt;Both pages follow the same pattern: a centered form, controlled inputs, form submission that calls the auth service, error display, and a redirect on success.&lt;/p&gt;

&lt;p&gt;The register page collects username, email, password, and confirm password. It validates that passwords match before making the API call. On success, it stores the tokens and redirects to the dashboard.&lt;/p&gt;

&lt;p&gt;The login page collects email and password. On success, it stores the tokens and redirects to the &lt;code&gt;next&lt;/code&gt; query parameter URL if present, or the dashboard if not.&lt;/p&gt;

&lt;p&gt;Both pages check if a user is already authenticated on mount and redirect to the dashboard. If so, logged-in users shouldn't see the login and register pages.&lt;/p&gt;

&lt;p&gt;Error handling on both pages displays the error message returned from the Django API directly to the user, "Invalid email or password" or field-specific validation errors from the registration serializer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Environment Variables
&lt;/h2&gt;

&lt;p&gt;Two environment files are created today.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;.env.local&lt;/code&gt; for development. &lt;code&gt;NEXT_PUBLIC_API_URL&lt;/code&gt; set to &lt;code&gt;http://localhost:8000/api&lt;/code&gt;. This is never committed to version control.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;.env.local.example&lt;/code&gt; showing the required keys without values, committed to the repository, so anyone cloning the project knows what environment variables to set.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; prefix is required for any environment variable that needs to be accessible in the browser. Variables without this prefix are server-side only.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Things Stand After Day 91
&lt;/h2&gt;

&lt;p&gt;The authentication foundation is complete on the frontend. Users can register, log in, and log out. The token refresh happens automatically. Protected pages redirect unauthenticated users to login. The Navbar reflects the auth state. The Axios instance handles Authorization headers on every request.&lt;/p&gt;

&lt;p&gt;Every subsequent day of frontend work builds on this foundation. The Axios instance means no component ever needs to manually attach tokens. The auth context means no component ever needs to read from localStorage. The ProtectedRoute means no page ever needs to reimplement authentication checks.&lt;/p&gt;

&lt;p&gt;Thanks for reading. Feel free to share your thoughts!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>buildinpublic</category>
      <category>100daysofcode</category>
    </item>
    <item>
      <title>Day 90 of #100DaysOfCode — DevCollab: Collaboration Requests API and Search</title>
      <dc:creator>M Saad Ahmad</dc:creator>
      <pubDate>Sun, 03 May 2026 08:10:48 +0000</pubDate>
      <link>https://forem.com/m_saad_ahmad/day-90-of-100daysofcode-devcollab-collaboration-requests-api-and-search-50d6</link>
      <guid>https://forem.com/m_saad_ahmad/day-90-of-100daysofcode-devcollab-collaboration-requests-api-and-search-50d6</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the Collaboration Requests API Needs to Do
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Four operations, each with distinct permission requirements:&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Accepting or rejecting a request, only the project owner can change a request's status. The requester cannot accept or reject their own request.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Four operations, four distinct permission scenarios. Getting this right is the most important part of today.&lt;/p&gt;




&lt;h2&gt;
  
  
  The CollaborationRequestSerializer
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;For the write side, the serializer only exposes the &lt;code&gt;message&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;SerializerMethodField&lt;/code&gt; for &lt;code&gt;project_detail&lt;/code&gt; 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.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Permission Logic
&lt;/h2&gt;

&lt;p&gt;The collaboration request endpoints need two custom permission behaviors that don't exist in DRF's built-in classes.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The second is checking that the user modifying a request status is the project owner, not the requester. The &lt;code&gt;IsProjectOwner&lt;/code&gt; permission class handles this. It implements &lt;code&gt;has_object_permission&lt;/code&gt; on the &lt;code&gt;CollaborationRequest&lt;/code&gt; object, checking &lt;code&gt;obj.project.owner == request.user&lt;/code&gt; rather than &lt;code&gt;obj.owner == request.user&lt;/code&gt;. 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.&lt;/p&gt;




&lt;h2&gt;
  
  
  The ViewSet Structure
&lt;/h2&gt;

&lt;p&gt;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 &lt;code&gt;/api/projects/&amp;lt;project_id&amp;gt;/requests/&lt;/code&gt; rather than at their own top-level route.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;get_queryset&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;perform_create&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;The status update action, accepting or rejecting, is a separate action rather than using the default &lt;code&gt;partial_update&lt;/code&gt;. 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.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Mine Endpoint
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;mine&lt;/code&gt; endpoint lives outside the nested project URL structure; it's at &lt;code&gt;/api/requests/mine/&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Handling the unique_together Constraint Gracefully
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;unique_together&lt;/code&gt; constraint on the &lt;code&gt;CollaborationRequest&lt;/code&gt; model, one request per user per project, is enforced at the database level. When a second request is attempted, the database raises an &lt;code&gt;IntegrityError&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Completing the URL Structure
&lt;/h2&gt;

&lt;p&gt;After today, the complete URL map for the backend looks like this:&lt;/p&gt;

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

&lt;p&gt;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 &lt;code&gt;path()&lt;/code&gt; definitions give more control over the exact URL shape.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing the Complete Backend
&lt;/h2&gt;

&lt;p&gt;With the collaboration requests API done, today's Postman testing covers the full user journey end-to-end.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;mine&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;Every success case and every failure case is tested. The backend is only trustworthy if the rejections work as reliably as the acceptances.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Things Stand After Day 90
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Thanks for reading. Feel free too share your thougts!&lt;/p&gt;

</description>
      <category>api</category>
      <category>backend</category>
      <category>buildinpublic</category>
      <category>django</category>
    </item>
    <item>
      <title>Day 89 of #100DaysOfCode — DevCollab: Building the Projects API with DRF</title>
      <dc:creator>M Saad Ahmad</dc:creator>
      <pubDate>Sat, 02 May 2026 10:31:25 +0000</pubDate>
      <link>https://forem.com/m_saad_ahmad/day-89-of-100daysofcode-devcollab-building-the-projects-api-with-drf-5h62</link>
      <guid>https://forem.com/m_saad_ahmad/day-89-of-100daysofcode-devcollab-building-the-projects-api-with-drf-5h62</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Approach: ViewSet Over Individual Views
&lt;/h2&gt;

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

&lt;p&gt;I went with &lt;code&gt;ModelViewSet&lt;/code&gt;. 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 &lt;code&gt;mine&lt;/code&gt; action.&lt;/p&gt;

&lt;p&gt;This is exactly the DRF design philosophy: write the parts that are unique to your app, let the framework handle the boilerplate.&lt;/p&gt;




&lt;h2&gt;
  
  
  Serializers — More Than Just Field Mapping
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;ProjectSerializer&lt;/code&gt; does more than map model fields to JSON. Three things make it interesting.&lt;/p&gt;

&lt;p&gt;First, the &lt;code&gt;tech_stack_list&lt;/code&gt; and &lt;code&gt;roles_list&lt;/code&gt; 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. &lt;code&gt;SerializerMethodField&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;/api/projects/1/&lt;/code&gt; gives the frontend everything it needs to render the project detail page without a second request to fetch the owner's profile. &lt;code&gt;SerializerMethodField&lt;/code&gt; handles this too, using the &lt;code&gt;UserSerializer&lt;/code&gt; from the accounts app to serialize the owner.&lt;/p&gt;

&lt;p&gt;Third, the &lt;code&gt;request_status&lt;/code&gt; 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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Permissions — The IsOwner Class
&lt;/h2&gt;

&lt;p&gt;Django REST Framework's built-in permission classes handle authentication; &lt;code&gt;IsAuthenticated&lt;/code&gt; 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.&lt;/p&gt;

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

&lt;p&gt;The &lt;code&gt;has_permission&lt;/code&gt; method handles request-level permission, before DRF even fetches the object. Safe methods like GET and HEAD always pass. Write methods require authentication.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  The ViewSet — Bringing the Pieces Together
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;ProjectViewSet&lt;/code&gt; is where everything connects. Four customized behaviors on top of what &lt;code&gt;ModelViewSet&lt;/code&gt; provides automatically.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;&lt;code&gt;get_permissions&lt;/code&gt;&lt;/strong&gt;: 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 &lt;code&gt;mine&lt;/code&gt; action requires authentication only. &lt;code&gt;get_permissions&lt;/code&gt; returns the right combination of permission classes based on &lt;code&gt;self.action&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;perform_create&lt;/code&gt;&lt;/strong&gt;: When a project is created, the owner is set to &lt;code&gt;request.user&lt;/code&gt; 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.&lt;/p&gt;

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




&lt;h2&gt;
  
  
  Search and Filter in Practice
&lt;/h2&gt;

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

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

&lt;p&gt;The &lt;code&gt;icontains&lt;/code&gt; lookup is important here; it uses SQL &lt;code&gt;LIKE&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Q&lt;/code&gt; 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 &lt;code&gt;Q&lt;/code&gt; objects, a filter would require the keyword to appear in all fields at once, which is never what you want for search.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Router
&lt;/h2&gt;

&lt;p&gt;The router is one of DRF's most satisfying features. Registering &lt;code&gt;ProjectViewSet&lt;/code&gt; with the router generates all of these URL patterns automatically:&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;mine&lt;/code&gt; action on its own URL. Six standard endpoints and one custom one, all from a single &lt;code&gt;router.register()&lt;/code&gt; call.&lt;/p&gt;

&lt;p&gt;The router is included in the main project URLs under the &lt;code&gt;/api/&lt;/code&gt; prefix, so all project endpoints live under &lt;code&gt;/api/projects/&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing in Postman
&lt;/h2&gt;

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

&lt;p&gt;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 &lt;code&gt;mine&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Things Stand After Day 89
&lt;/h2&gt;

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

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Thanks for reading. Feel free to share your thoughts!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>buildinpublic</category>
      <category>100daysofcode</category>
    </item>
  </channel>
</rss>
