DEV Community

Cover image for Code, Coffee & Chaos: How I Built and Launched My Angular Micro SaaS MVP
Karol Modelski
Karol Modelski

Posted on • Originally published at karol-modelski.Medium

Code, Coffee & Chaos: How I Built and Launched My Angular Micro SaaS MVP

Code, Coffee & Chaos: How I Built and Launched My Angular Micro SaaS MVP

Welcome to part two of “Zero to SaaS in 14 Days” — my real-world series where I tackle building, launching, and documenting a SaaS product in just two weeks. In part one, I built a Subscription Tracker from scratch, fighting through deadline pressure and momentum swings to deliver a working MVP. Now the adventure ramps up.

This time, I’m deep-diving into the creation of a job application tracker, an idea sparked by my own battle with cluttered notes, endless follow-ups, and missed opportunities. As a senior Angular developer, I wanted to combine speed, best practices, and a little bit of chaos — all while learning fast and delivering faster.

Get ready for practical decisions, unconventional Angular tips, and a front-row seat to the drama and satisfaction of solo SaaS building. Whether you code in Angular or just want to see a product rise from nothing, you’re in the right place.

💡 The Spark: What Problem Was I Solving?

Like many of us on the job hunt, I was juggling multiple applications, each at a different stage, all scrambling for my attention. My notes lived in scattered spreadsheets and sticky notes, my brain a fog of due dates and follow-ups. One night after missing a deadline, I realized this chaos was a shared pain point.

I envisioned a clean, minimal app that could bring order to the frenzy — a dashboard tracking statuses, a simple drag-and-drop to move applications through stages, and gentle reminders nudging me when I needed to act. It was like crafting a personal assistant in code.

🔍 But Would Anyone Actually Use It?

Until now, this was my secret project. I hesitated to share it publicly, fearing judgment or failure. But the desire to connect and learn outweighed the fear. Now, I’m excited to present this MVP here — unvarnished and real. It’s a tool I built for myself, and now I invite you to try it, poke at it, and tell me honestly what you think.

By sharing here first, I hope to gather authentic feedback from fellow developers, job seekers, and curious minds. Your insights will help shape the MVP into a product worth sharing with more people. This moment marks a pivotal shift — from solo tinkering to community-driven growth.

💡 Ready to master your job search? Try the FREE ApplyTrack app and say goodbye to missed deadlines 📆, forgotten follow-ups 🛎️, and job hunt chaos 📋. Get organized and track your progress the easy way! Click to join the beta — a smarter career journey is just one signup away! 🚀✨

ApplyTrack | Job Application Tracker & Smart Job Search Organizer

Organize your job search with ApplyTrack. Track applications, set deadlines, and get reminders on one easy dashboard. Stay on top of your job hunt today.

apply-track.carrd.co

⚙️ Why Angular? The Tech Behind the Magic

Strong Foundation with Angular

As a senior developer, Angular felt like a trusted, capable partner. Its TypeScript foundation gave me confidence and clarity. Leveraging Angular Material components, I built a UI that felt polished and professional early on. Applying best practices in modularity and reactive programming, I crafted an app that is maintainable and robust, designed to grow beyond the MVP.

⚛️ Simple State Management with Angular Signals

I wanted to keep complexity low. Enter Angular Signals — an elegant way to manage reactive state natively in components. Signals allowed me to easily track application data and UI state, react instantly to changes, and keep the architecture clean without the overhead of Redux or other state libraries.

Here’s how I updated my application list reactively:

private addNewApplication(application: Omit<JobApplication, 'id'>): void {
    const newApplication: JobApplication = {
      ...application,
      id: this.generateId(),
    };

    // Add to the appropriate status array
    switch (newApplication.status) {
      case 'applied':
        this.appliedApplications.update((apps) => [...apps, newApplication]);
        break;
      case 'interview':
        this.interviewApplications.update((apps) => [...apps, newApplication]);
        break;
      case 'offer':
        this.offerApplications.update((apps) => [...apps, newApplication]);
        break;
      case 'rejected':
        this.rejectedApplications.update((apps) => [...apps, newApplication]);
        break;
    }

    // Effects will automatically handle computed value updates
  }

  private updateExistingApplication(
    applicationId: string,
    updatedData: Omit<JobApplication, 'id'>
  ): void {
    const updatedApplication: JobApplication = {
      ...updatedData,
      id: applicationId,
    };

    // Find and remove from current array
    const allArrays = [
      this.appliedApplications,
      this.interviewApplications,
      this.offerApplications,
      this.rejectedApplications,
    ];

    for (const arraySignal of allArrays) {
      const applications = arraySignal();
      const index = applications.findIndex((app) => app.id === applicationId);

      if (index !== -1) {
        // Remove from current array
        const updatedApps = applications.filter((app) => app.id !== applicationId);
        arraySignal.set(updatedApps);
        break;
      }
    }

    // Add to the appropriate status array based on new status
    switch (updatedApplication.status) {
      case 'applied':
        this.appliedApplications.update((apps) => [...apps, updatedApplication]);
        break;
      case 'interview':
        this.interviewApplications.update((apps) => [...apps, updatedApplication]);
        break;
      case 'offer':
        this.offerApplications.update((apps) => [...apps, updatedApplication]);
        break;
      case 'rejected':
        this.rejectedApplications.update((apps) => [...apps, updatedApplication]);
        break;
    }

    // Effects will automatically handle computed value updates
  }
Enter fullscreen mode Exit fullscreen mode

This approach kept the app reactive and snappy, making the development process joyful instead of tedious.

Toolbox Highlights

  • Angular Material for crafting a sleek and consistent UI
  • Angular CDK Drag-and-Drop to bring fluid interactivity
  • Reactive Forms to enforce sane, user-friendly data entry
  • Angular Signals for intuitive, performant state control

🧩 Peek at the Code

Drag and Drop Status Update

One of the moments that made me smile was the drag-and-drop feature working for the first time — applications moved easily from one stage to another, and data stayed perfectly in sync:

drop(event: CdkDragDrop<JobApplication[]>) {
    if (event.previousContainer === event.container) {
      // Reordering within the same column - work with original arrays
      const containerId = event.container.id;
      const originalArray = this.getOriginalArrayFromContainerId(containerId);

      // Find the actual indices in the original array
      const draggedItem = event.container.data[event.previousIndex];
      const actualPrevIndex = originalArray().findIndex(
        (app: JobApplication) => app.id === draggedItem.id
      );

      // For reordering, we need to find where to insert in the original array
      // This is complex with filtering, so for now we'll skip reordering when filters are active
      if (this.searchTerm() === '' && this.selectedStatus() === 'all') {
        moveItemInArray(originalArray(), actualPrevIndex, event.currentIndex);
        this.updateFilteredApplications();
      }
    } else {
      // Moving between columns - update the application status
      const application = event.previousContainer.data[event.previousIndex];
      const newStatus = this.getStatusFromContainerId(event.container.id);
      const oldStatus = this.getStatusFromContainerId(event.previousContainer.id);

      // Remove from old array
      const oldArray = this.getOriginalArrayFromContainerId(event.previousContainer.id);
      const oldApplications = oldArray().filter((app: JobApplication) => app.id !== application.id);
      oldArray.set(oldApplications);

      // Update application status and add to new array
      const updatedApplication = { ...application, status: newStatus };
      const newArray = this.getOriginalArrayFromContainerId(event.container.id);
      const newApplications = [...newArray(), updatedApplication];
      newArray.set(newApplications);

      // Effects will automatically handle updates
    }
  }
Enter fullscreen mode Exit fullscreen mode

Reactive Form Setup for Adding Applications

Building the add application modal was a lesson in balancing validation with user experience. Each form control felt like a conversation: “Tell me the company name,” “And the position title,” guiding the user while gently enforcing correctness.

readonly applicationForm = new FormGroup({
    company: new FormControl(this.data?.application?.company || '', {
      validators: [Validators.required, Validators.minLength(2)],
      nonNullable: true,
    }),
    position: new FormControl(this.data?.application?.position || '', {
      validators: [Validators.required, Validators.minLength(2)],
      nonNullable: true,
    }),
    status: new FormControl<ApplicationStatus>(this.data?.application?.status || 'applied', {
      validators: [Validators.required],
      nonNullable: true,
    }),
    dateApplied: new FormControl(this.data?.application?.dateApplied || new Date(), {
      validators: [Validators.required],
      nonNullable: true,
    }),
    notes: new FormControl(this.data?.application?.notes || '', { nonNullable: true }),
    reminderDate: new FormControl<Date | null>(this.data?.application?.reminderDate || null),
    reminderType: new FormControl<'follow-up' | 'interview' | 'decision-deadline'>(
      this.data?.application?.reminderType || 'follow-up',
      { nonNullable: true }
    ),
    reminderDescription: new FormControl(this.data?.application?.reminderDescription || '', {
      nonNullable: true,
    }),
  });

  onSubmit(): void {
    if (this.applicationForm.valid) {
      const formValue = this.applicationForm.getRawValue();
      const result: AddApplicationDialogResult = {
        application: {
          company: formValue.company,
          position: formValue.position,
          status: formValue.status,
          dateApplied: formValue.dateApplied,
          notes: formValue.notes || undefined,
          reminderDate: formValue.reminderDate || undefined,
          reminderType: formValue.reminderType,
          reminderDescription: formValue.reminderDescription || undefined,
        },
      };
      this.dialogRef.close(result);
    }
  }
Enter fullscreen mode Exit fullscreen mode

Dashboard Stats With Angular Material Cards

Seeing stats update in real-time became a small daily delight — a tangible measure of progress on the job hunt amidst the chaos.

<div class="stats-container">
  @for (let stat of statsList; track stat.id) {
    <mat-card class="stat-card">
      <mat-card-content>
        <div class="stat-value">{{ stat.value }}</div>
        <div class="stat-label">{{ stat.label }}</div>
      </mat-card-content>
    </mat-card>
  }
</div>

this.statsList = [
  { label: 'Total Applications', value: this.stats.total },
  { label: 'Applied', value: this.stats.applied },
  { label: 'Interviews', value: this.stats.interviews },
  { label: 'Offers', value: this.stats.offers },
  { label: 'Success Rate', value: this.stats.successRate + '%' }
];
Enter fullscreen mode Exit fullscreen mode

Reminder Logic With Urgency

Late nights spent perfecting the reminder logic felt worth it when the app started nudging me ahead of deadlines, feeling like a thoughtful assistant:

private updateReminders() {
    const currentDate = new Date();
    const reminderWindow = 14; // 14 days ahead
    const futureDate = new Date();
    futureDate.setDate(currentDate.getDate() + reminderWindow);

    const allApplications = this.getAllApplications();
    const applicationsWithReminders = allApplications.filter(
      (app) => app.reminderDate && app.reminderType && app.reminderDescription
    );

    const computedReminders: ComputedReminder[] = applicationsWithReminders.map((app) => {
      const reminderDate = app.reminderDate!;
      const timeDiff = reminderDate.getTime() - currentDate.getTime();
      const daysUntilDue = Math.ceil(timeDiff / (1000 * 3600 * 24));

      return {
        applicationId: app.id,
        company: app.company,
        position: app.position,
        type: app.reminderType!,
        date: reminderDate,
        description: app.reminderDescription!,
        isOverdue: reminderDate < currentDate,
        daysUntilDue,
      };
    });

    // Separate overdue and upcoming reminders
    const overdue = computedReminders
      .filter((reminder) => reminder.isOverdue)
      .sort((a, b) => b.date.getTime() - a.date.getTime()); // Most recent overdue first

    const upcoming = computedReminders
      .filter(
        (reminder) =>
          !reminder.isOverdue && reminder.date >= currentDate && reminder.date <= futureDate
      )
      .sort((a, b) => a.date.getTime() - b.date.getTime()); // Soonest first

    this.overdueReminders.set(overdue);
    this.upcomingReminders.set(upcoming);
  }
Enter fullscreen mode Exit fullscreen mode

✨ Want simple, powerful online tools that just work? Check out my apps! I build user-friendly software designed to save you money and make your life easier. 🚀 Sign up now and grab your FREE 10-Step Code Review Checklist 📋 - level up your projects today!

Transforming Ideas to Apps by Karol Modelski – Tech Inventor

Discover a portfolio that fuses Angular expertise with tech innovation—showcasing scalable web apps, mobile projects, and digital products built by a solopreneur architect and inventor. Unlock high-performance solutions and creative inspiration in one place.

karol-modelski.carrd.co

🗣️ Your Feedback Matters — Let’s Shape This Together!

Building this micro SaaS MVP has been an exhilarating journey full of learning and late-night coding sprints. Now that the app is shared here for the first time, I’m eager to hear your thoughts. Whether you’re a developer, job seeker, or just curious, your feedback will shape the app’s next chapters.

  • Which features stand out?
  • What could make the app even more helpful?
  • How intuitive does it feel to you?
  • Found any quirks or bugs?

Drop a comment below or reach out on X or LinkedIn. Your voices will guide future updates and help build something truly valuable together.

🏗️ Building, Breaking, Fixing: The Development Rollercoaster

Iteration 1: The Barebones MVP

Building core features was rewarding, but unexpected bugs taught me patience. One memorable night, I wrestled with a drag-and-drop reorder bug caused by filtered views. The fix was humble yet crucial: disabling reordering during filtering. A small patch, but a big sigh of relief.

Iteration 2: Powering Up Reminders and Stats

Adding reminders raised the bar. Dates, timezones, urgency — suddenly complexity exploded. I remember a caffeine-fueled debugging marathon that ended with finally squashing a timezone bug. Those moments define the startup grind: frustrating but ultimately satisfying.

🎓 Lessons from the Trenches

Technical Growth

Mastering Angular Signals revolutionized my state management approach. Complex date handling became a trusted friend instead of a foe.

Entrepreneurial Mindset

Owning every piece — from code to customer conversations — deepened my appreciation for user feedback and rapid iteration.

Balance and Focus

Sacrificing evenings and weekends was tough but necessary. Leveraging efficient tooling kept my momentum.

Validation

While formal validation is yet to come, early feedback and insights from this article’s readers will be invaluable. Hearing from you will help steer the product’s next steps, proving why every bug fixed and every feature crafted matters.

🎉 Wrapping Up: Your SaaS Adventure Awaits

This journey is a testament to persistence and passion. Whether starting or scaling, building a SaaS product is a wild ride packed with lessons and rewards. If you have an idea, start now. Validate fast, learn fast, iterate fast.

What’s your story? Comment below or connect on X/LinkedIn. And if you want to try the app, it’s live at https://apply-track.carrd.co/.

ApplyTrack | Job Application Tracker & Smart Job Search Organizer

Organize your job search with ApplyTrack. Track applications, set deadlines, and get reminders on one easy dashboard. Stay on top of your job hunt today.

apply-track.carrd.co

Let’s build something incredible together!


Thanks for Reading 🙌

I hope these tips help you ship better, faster, and more maintainable frontend projects.

🚀 Check Out My Portfolio
Discover SaaS products and digital solutions I've built, showcasing expertise in scalable architectures and modern frontend tech.
👉 View Portfolio

🛠 Explore My Developer Resources
Save time and level up your code reviews, architecture, and performance optimization with my premium Angular & frontend tools.
👉 Browse on Gumroad

💬 Let's Connect on LinkedIn
I share actionable insights on Angular & modern frontend development - plus behind‑the‑scenes tips from real‑world projects.
👉 Connect with me here

📣 Follow Me on X
Stay updated with quick frontend tips, Angular insights, and real-time updates - plus join conversations with other developers.
👉 Follow me on X

Your support fuels more guides, checklists, and tools for the frontend community.

Let's keep building together 🚀

Top comments (2)

Collapse
 
shemith_mohanan_6361bb8a2 profile image
shemith mohanan

Brilliant walkthrough, Karol — totally relate to the solo SaaS journey and those late-night “bug battles.” Angular Signals was a smart choice — that reactivity makes small apps feel enterprise-grade. Excited to try ApplyTrack and see how it evolves! 💡

Collapse
 
karol_modelski profile image
Karol Modelski

Thanks! Those bug battles are part of the solo SaaS fun. Angular Signals keep things smooth without extra hassle. Can’t wait for you to try ApplyTrack and share your thoughts. If you want in, sign up for the beta here: karol-modelski.kit.com/55b9e7ffa2 — would love to have you onboard!