# Screaming Architecture: the key to a scalable frontend

> Your frontend project should scream what it does, not what it is built with. Concrete migration examples, framework coexistence with Nuxt, and shared state handling.

## The pattern I saw everywhere

Across multiple companies where I worked, I kept running into the same pattern. Teams knew that organizing by features was important, the intention was there. But in practice, the feature ended up scattered across the entire project: `components/auth/`, `services/auth/`, `store/auth/`, `views/auth/`. The feature name appeared in every technical folder, but never in a single place.

```text
src/
├── components/
│   ├── auth/          # LoginButton.vue, RegisterForm.vue
│   ├── products/      # ProductCard.vue, ProductList.vue
│   └── orders/        # OrderSummary.vue
├── views/
│   ├── auth/          # LoginView.vue, RegisterView.vue
│   ├── products/      # ProductsView.vue
│   └── orders/        # OrdersView.vue
├── services/
│   ├── auth.service.ts
│   ├── products.service.ts
│   └── orders.service.ts
├── store/
│   ├── auth.store.ts
│   ├── products.store.ts
│   └── orders.store.ts
└── utils/
```

At first glance it looks organized. But when you need to change something in Auth, you touch 4 folders. When a new dev joins, they have to mentally reconstruct which files belong to which feature. And as the app grows, each technical folder becomes a junk drawer with 30+ files.

The problem isn't a lack of organization. It's that the organization screams "technology" instead of screaming "business."

## The analogy that explains it all

Imagine a house blueprint. You don't see a "bricks" section, a "cement" section, and a "windows" section. You see "kitchen," "bedroom," "bathroom." The blueprints scream the purpose of each space, not the materials it's built with.

Screaming Architecture seeks the same for your code. Your top-level folder structure should scream the features of your application, not the technologies you use.

## What you gain with this approach

- **Instant understanding**: you open the project and at a glance you know what the business does.
- **Fast onboarding**: new devs understand the project structure in minutes, not days.
- **Localized changes**: modifying a feature doesn't force you to touch 4 folders. Everything is in one place.
- **Real scalability**: adding a new feature means creating a folder, not inserting files in 6 different places.
- **Easier testing**: each module is an isolated unit you can test independently.
- **Safe refactoring**: moving or deleting an entire feature means moving or deleting a folder.

## Applying Screaming Architecture

The idea is simple: group by features or business domains. Each folder contains everything needed for that functionality.

```text
src/
├── modules/
│   ├── Auth/
│   │   ├── components/   # LoginButton.vue, RegisterForm.vue
│   │   ├── views/        # LoginView.vue, RegisterView.vue
│   │   ├── routes/       # Routes for this module
│   │   ├── store/        # auth.store.ts
│   │   └── services/     # auth.service.ts
│   ├── Products/
│   │   ├── components/   # ProductCard.vue, ProductList.vue
│   │   ├── views/        # ProductsView.vue, ProductDetailView.vue
│   │   ├── store/        # products.store.ts
│   │   └── services/     # products.service.ts
│   └── Orders/
│       ├── components/
│       ├── views/
│       ├── store/
│       └── services/
├── shared/
│   ├── ui/
│   │   ├── components/   # BaseButton.vue, ModalBase.vue
│   │   └── composables/  # useModal.ts, useDarkMode.ts
│   ├── composables/      # useDebounce.ts, useLocalStorage.ts
│   ├── utils/            # formatDate.ts, validateEmail.ts
│   └── assets/           # Global images, base styles
├── layouts/              # DefaultLayout.vue, AuthLayout.vue
├── router/               # General router
├── app.vue
└── main.ts
```

Not every module needs the same internal structure. Auth may have `store/` and `services/`, but a Landing module might just be `components/` and `views/`. Each module defines what it needs.

## From theory to practice: migrating a feature

Let's take the Auth example from the disaggregated structure and see what the migration looks like:

**Before** (4 folders):

```text
src/components/auth/LoginButton.vue
src/components/auth/RegisterForm.vue
src/views/auth/LoginView.vue
src/views/auth/RegisterView.vue
src/services/auth.service.ts
src/store/auth.store.ts
```

**After** (1 folder):

```text
src/modules/Auth/components/LoginButton.vue
src/modules/Auth/components/RegisterForm.vue
src/modules/Auth/views/LoginView.vue
src/modules/Auth/views/RegisterView.vue
src/modules/Auth/services/auth.service.ts
src/modules/Auth/store/auth.store.ts
```

The imports change, the code doesn't. If your Auth module needs to expose something to the rest of the app (a guard, a composable, the user state), you can create an `index.ts` that serves as the module's public API:

```ts
// src/modules/Auth/index.ts
export { useAuthStore } from './store/auth.store'
export { useCurrentUser } from './composables/useCurrentUser'
export { authGuard } from './routes/guards'
```

The rest of the app imports from `@/modules/Auth`, not from internal folders. If you refactor the module's internal structure, external imports don't break.

## Framework coexistence

If you use Nuxt, Next, or any framework with folder conventions (`pages/`, `composables/`, `components/`), the obvious question is: how does this coexist with Screaming Architecture?

Framework conventions handle routing and auto-imports. Screaming Architecture handles business logic.

In Nuxt, for example, `pages/` defines routes, but the page can be a thin wrapper that imports the actual module:

```vue
<!-- pages/auth/login.vue -->
<template>
  <LoginView />
</template>

<script setup lang="ts">
import LoginView from '~/modules/Auth/views/LoginView.vue'
</script>
```

Nuxt handles routing (`/auth/login`), and your Auth module handles the logic. `pages/` stays thin, just an entry point.

The same applies to `composables/`. Global composables (auto-imported by Nuxt) go in the framework's conventional folder. Feature-specific composables go inside the module:

```text
composables/              # Auto-imported by Nuxt (global)
├── useTheme.ts
└── useBreakpoint.ts
modules/Auth/composables/ # Auth-specific (manual import)
├── useCurrentUser.ts
└── useAuthRedirect.ts
```

## Cross-cutting concerns: what goes in shared

The most common question when applying this: what happens when two modules need the same thing?

The rule is simple. If something is specific to a feature, it goes in its module. If two or more features use it, it goes in `shared/`.

`shared/ui/` is for generic UI components with no business logic: buttons, modals, inputs. They're the building blocks, not the features.

`shared/composables/` is for reusable logic without business state: `useDebounce`, `useLocalStorage`, `useIntersectionObserver`.

`shared/utils/` is for pure functions: `formatDate`, `slugify`, `validateEmail`.

What about shared state? If Auth and Orders both need user data, Auth owns the state. Orders imports `useCurrentUser` from `@/modules/Auth`. If over time more modules need the same data, you can move that composable to `shared/`. But don't do it preemptively. Start with the owning module and only move when there's real evidence it's cross-cutting.

## When to use it and when not to

This approach works best for medium to large projects with real business logic and multi-dev teams. If your app has 5+ features that grow independently, Screaming Architecture will bring order to your life.

For MVPs, landing pages, or simple CRUDs, it might be overkill. You don't need a module structure for 3 views and a form.

If your project has the potential to grow, adopting it early saves you the painful migration later. And if you already have a large project with a generic structure, you can migrate progressively, one module at a time, starting with the feature that grows the most.

Next time you start a project, think about what the structure will look like when the team grows. If your folders only say `components/` and `utils/`, you know where to start.

I wrote that thinking about human teams. Today the newcomer who opens your repo is increasingly an AI agent, one that reads the structure just like a new dev but without the intuition to fill in the gaps. Taking this same idea all the way, so the repo not only screams its intent but also can't lie about itself, is what [Context Architecture](/en/blog/context-architecture-the-whole-repo-is-the-context) is about.
