Alexander Tkačenko
2025-08-28 5 min read

Multi-entry-point folder structure and file conventions for scalable web apps

The design aspects discussed here seek to address the following real-life issues:

Here’s how the code of an application can be arranged to address the scalability issues outlined above and to keep it clean and easy to navigate.

First off, we’ll put all our app’s code in the πŸ“ src directory to draw a clear boundary between the application’s own human-readable (and often human-made) code and various auxiliaries, like πŸ“ node_modules, tool configs, a πŸ“ dist directory with build artifacts, that will mostly stay outside πŸ“ src.

πŸ“ src
    πŸ“ const
    πŸ“ utils
    πŸ“ types
    πŸ“ public
    πŸ“ server
        πŸ“ const
        πŸ“ utils
        πŸ“ types
        πŸ“ middleware
        πŸ“„ index.ts // runnable app server combining entry points
    πŸ“ ui // components shared across multiple entries
    πŸ“ entries
        πŸ“ [entry-name] // can be "main" for a start
            πŸ“ const
            πŸ“ utils
            πŸ“ types
            πŸ“ public
            πŸ“ server
                πŸ“ const
                πŸ“ utils
                πŸ“ types
                πŸ“ middleware
                πŸ“„ index.ts // exports Express Router (or similar)
            πŸ“ ui
                πŸ“ [feature-name] // can be "app" or skipped in the beginning
                    πŸ“ const
                    πŸ“ utils
                    πŸ“ types
                    πŸ“ Component
                        πŸ“„ index.tsx // exports Component and type ComponentProps
                        πŸ“„ index.css
                πŸ“„ index.tsx // optional CSR entry point
    πŸ“ lib // features as packages, patched third-party packages
        πŸ“ [lib-name]

The runnable application server is located in πŸ“„ src/server/index.ts, which is pretty straightforward to spot without prior knowledge of the codebase. While most directories shown above are optional and can be added as needed, note how they create a recurrent pattern from topmost to inmost parts allowing for common conventions being reused over and over.

Subdirectories of πŸ“ entries contain the app’s entry points. A few typical use cases for different entry points include: an older and newer tech stack in the same app, an older and newer UI within a single app, a main app with a lighter marketing landing page or a user onboarding app, or multiple self-contained portions of a single app in general. An entry point can also serve an API to the rest of the app. Each entry point doesn’t have to map to a single route, but it’s convenient to have one parent route for an entry point.

As shown above, the app’s entry points replicate the basic app structure, too. They can be regarded as self-contained quasi-apps that can act largely independently from each other. For this same reason, cross-entry-point imports are strongly discouraged. Besides reducing the cognitive load of managing intertwined parts, this precaution also makes connecting and disconnecting an entry point nearly effortless.

Each level of the app, inside and outside the πŸ“ entries directory, can contain auxiliary files arranged into the directories πŸ“ const, πŸ“ utils, πŸ“ types, and optionally other domain-specific ones like πŸ“ middleware.

To facilitate navigation through the codebase, we should make file names very straightforward and transparent about their contents. The common file managing convention boils down to the following rules: (1) Single export per file. This rule still allows to collocate the main export with a tightly related type export, such as a function’s parameters type, in the same file, which is a good practice. (2) Files are named exactly as their export. With the same casing. For index files, this rule applies to the parent directory’s name.

πŸ“ const
    πŸ“„ customValue.ts // export const customValue

πŸ“ utils
    πŸ“„ getCustomValue.ts // export function getCustomValue

πŸ“ types
    πŸ“„ CustomType.ts // export type CustomType

For the sake of clarity of the codebase, using index files should be limited to small atomic parts. Large barrel index files listing re-exports complicate the codebase navigation and create ambiguous module access points. Index files can be used, for example, for main exports of a self-contained feature or an app component (having πŸ“„ Component/index.tsx instead of πŸ“„ Component/Component.tsx is fine).

Note that public assets can be split across entry points and served independently by each entry point through their own πŸ“ public directories to maintain a higher level of autonomy. To avoid duplication, resources shared across multiple entry points can still be located in the app’s top-level πŸ“ src/public directory served from the app’s πŸ“ server.

~

Adding these elements to the app’s codebase should make it more flexible regarding the common scalability issues and more comfortable to work with.