DEV Community

David J Song
David J Song

Posted on

Why I Chose Clean Architecture for a Social Weather App

When I started building ONNS (옷늘날씨), a weather-driven outfit community app, I had one recurring thought: “How do I stop my codebase from becoming a tangled ball of JavaScript spaghetti when I just want to tell people what to wear?”

That is when I decided to lean into Clean Architecture. Not because it is trendy, but because I wanted to keep things maintainable when features inevitably pile up such as comments, filters, login, and realtime updates.


Why Clean Architecture?

In short, it is all about boundaries. Just like you would not wear flip-flops in a snowstorm, you do not want your React components directly poking at your database.

Clean Architecture encourages you to separate your project into layers:

Domain: The pure business rules. For ONNS, that means things like OutfitPost, WeatherTag, and Comment.

Application: Use cases that orchestrate how features behave. For example, “create a new post tagged with today’s weather.”

Infrastructure: The part that talks to Supabase, Prisma, or any third-party service. The dirty work, kept isolated.

UI (App): Next.js pages, components, and hooks.

This way, if tomorrow I swap Supabase for Firebase or even raw SQL queries, the core logic does not care.


Example From the Repo

Here is a simplified example. Let us say I want to create a new post tagged with today’s weather.

Domain entity:

// backend/domain/entities/outfitPost.entity.ts
export interface OutfitPost {
  id: string;
  userId: string;
  imageUrl: string;
  feelsLike: number;
  season: 'SPRING' | 'SUMMER' | 'FALL' | 'WINTER';
  createdAt: Date;
}
Enter fullscreen mode Exit fullscreen mode

Application use case:

// backend/application/usecases/createPost.ts
import { OutfitPostRepository } from "../repositories/OutfitPostRepository";
import { OutfitPost } from "../../domain/entities/outfitPost.entity";

export class CreatePost {
  constructor(private repo: OutfitPostRepository) {}

  async execute(input: Omit<OutfitPost, 'id' | 'createdAt'>): Promise<OutfitPost> {
    const post: OutfitPost = {
      ...input,
      id: crypto.randomUUID(),
      createdAt: new Date(),
    };
    return await this.repo.save(post);
  }
}
Enter fullscreen mode Exit fullscreen mode

Infrastructure (Supabase adapter):

// backend/infrastructure/repositories/SupabaseOutfitPostRepository.ts
import { OutfitPost } from "../../domain/entities/outfitPost.entity";
import { OutfitPostRepository } from "../../application/repositories/OutfitPostRepository";
import { supabase } from "../../lib/supabase";

export class SupabaseOutfitPostRepository implements OutfitPostRepository {
  async save(post: OutfitPost): Promise<OutfitPost> {
    const { data, error } = await supabase.from('posts').insert(post).select().single();
    if (error) throw error;
    return data as OutfitPost;
  }
}
Enter fullscreen mode Exit fullscreen mode

UI layer:

// app/components/NewPostForm.tsx
import { CreatePost } from "@/backend/application/usecases/createPost";
import { SupabaseOutfitPostRepository } from "@/backend/infrastructure/repositories/SupabaseOutfitPostRepository";

export default function NewPostForm() {
  async function handleSubmit(formData: FormData) {
    const repo = new SupabaseOutfitPostRepository();
    const usecase = new CreatePost(repo);
    await usecase.execute({
      userId: "123",
      imageUrl: formData.get("image") as string,
      feelsLike: 12,
      season: "WINTER",
    });
  }

  return (
    <form action={handleSubmit}>
      {/* input fields for image, tags, etc. */}
      <button type="submit">Post</button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Notice how the NewPostForm does not know or care whether the data is going to Supabase, PostgreSQL, or even a JSON file on my desktop. That is the power of boundaries.


What This Bought Me

Maintainability: New features such as filtering outfits by season meant adding new use cases, not rewriting everything.

Flexibility: It could replace Supabase with another backend in theory without touching the UI or domain.

Clarity: When I opened the repo after two weeks, I did not feel like a stranger in my own house.


A Little Wit to End

Just like you would not layer a winter coat under your t-shirt, you should not jam your database calls inside your React components. Clean Architecture keeps your code dressed appropriately for any season.

And in ONNS, that is literally the point.


Have thoughts on Clean Architecture, or want to roast my choice of Supabase? Drop a comment!

Top comments (0)