Latest posts

Scalable folder structure for Next.js projects

Scalable folder structure for Next.js projects

Next.js and React offer great capabilities and flexibility, but with such freedom comes lot of question marks.One of them is the most underrated productivity boosts in a growing Next.js project - a clear and scalable folder structure. A good structure not only helps new developers onboard faster, but also makes refactoring and scaling easier.So let's get straight to it. Here is a pattern AND principles I have found effective when working with Next.js App Router projects (including localization).Remember: the principles (described later) are more important than folders themselves.├─ app│ └─🏠[locale] # Routes and layouts│ │ ├─ pageA│ │ └─ pageB│ ││ └─ 🔗api # API routes & webhooks│├─ 📦components│ ├─ layouts # Wrapper layouts│ ├─ shared # App wide layout (nav, footer)│ └─ ui # Smallest UI primitives│├─ ⚙️config # Centralised runtime & build config│ └─ app.config.ts│├─ 🌐i18n # Next-intl configuration│ └─ messages # Translations /en, /de│├─ 🧩modules # Business domain engines, facades│ ├─ database│ ├─ auth│ ├─ cms│ └─ ...│├─ 📡services # Technical utility wrappers│ ├─ api.ts│ └─ store.ts│├─ 🎨styles│ ├─ global.css # Global styles│ └─ tokens.css # Design tokens└─📂 Breakdown/app - Holds all routes, layouts, and route-specific UI. This is the file-system router entry point. Files inside define public and private pages, error boundaries, loading states, and layouts. Only colocate UI here if it belongs exclusively to that route./app/api - Contains API route handlers and webhooks. Each folder inside represents one endpoint. Use this layer only for request handling, delegating logic into modules/ or services/./components - Holds reusable UI components. These are framework-agnostic building blocks that do not contain domain logic. Split further into: - /layouts → wrapper layouts that combine providers and consistent shells.- /shared → widgets reused across pages or features (navigation, banners, forms).- /ui → smallest UI parts, often primitives generated from a design system (think for example Shadcn components)/config - Holds runtime and build configuration. Centralize environment variable parsing, validation, and app-wide constants here. This ensures no process.env usage leaks into components or modules./i18n - Holds internationalization resources and setup. Includes message catalogs, locale definitions, and translation helpers. Keeps all localization in one place for consistency./modules - Contains business domain logic and facades. Each subfolder represents a domain (auth, database, cms, payments). Inside live server actions, domain schemas, adapters to external systems, and domain-specific hooks. UI imports these modules through public actions, never directly from adapters. /services - Holds technical utilities and infrastructure wrappers. Examples: API clients, logging setup, analytics SDK wrappers, state management stores. These files describe how the system communicates, not what the business does./styles - Contains global styling resources. This includes the main global stylesheet and design tokens. Component or feature-specific styles should remain colocated with those components instead of here.🔑 Principles to keep this structure maintainable1. Co-locate until re-usedKeep components, hooks, and utilities close to the route, feature or component that uses them. For example, if a hook is used only on checkout page, keep it there. Only “lift it up” one level, (or into root components/ or modules/) once it’s shared across multiple routes. This avoids premature abstraction and keeps things tidy.Another benefit of co-location is that it keeps refactors local (PR's tidier) and prevents a monolithic files such as "types.ts" or "helpers.ts" in the root. You will avoid the "junk drawer" pattern, having giant folder or file, where we tend to put things if we don't know where they belong.2. Keep routing files purePurpose of the app/, is to be a file-system router. It should mainly contain Next.js routing files such as: page.tsx, layout.tsx, error.tsx, loading.tsx, etc., and colocated UI that is exclusive to that route - never used outside of that route.Domain logic, API clients, and reusable UI should live in it's own folders higher in the folder hierarchy.3. Use route groups for context and separationRoute groups "(...)" don't affect URL path, so use that to your advantage. They help you organize related routes together, for a cleaner hierarchy.├─ (auth) # Routes related to authentication│ ├─ login │ ├─ register│ └─ forgot-password│ ├─ (checkout) # Routes related to checkout│ ├─ cart │ ├─ payment│ └─ shopping-bag│ ├─ (dashboard) # Top-level context group│ ├─ (admin) # Sub-level admin related routes│ └─ (user) # Sub-level user related routes└─- Use route groups to mirror functional areas of the app.- Don’t over-segment → Groups should represent meaningful boundaries (auth, checkout, dashboard), not every small feature.- Combine with the “co-locate until re-used” principle: if a component is unique to (checkout), keep it inside that group; if reused, promote one level higher.4. One-way dependency flowIn order to avoid circular imports or bundling server-side libraries to client-side, it's important do define your data flow.Example:UI → Services → Modules → External APIsNote that each layer depends downward, never upward. Prevents circular imports - if UI imports a module, and the module imports back into UI, you will end up with runtime errors or tangled imports.Improves testability - you can test modules without mocking UI, and test services without needing domain logic.Supports scalability - clear separation lets you replace providers or swap frameworks (Axios → Fetch) without touching business logic or UI.⚡️Putting it all togetherGetting the structure right early saves dozens of hours of refactoring later. It shapes the developer experience every single day.I have turned these ideas into a living, breathing project — FrontendAccelerator.com which comes with this exact structure, clear docs, and a private Discord community where developers share best practices and get feedback on their setup. It’s designed so you don’t have to spend weeks wiring up folders, auth, payments, databases or AI integrations before seeing your app come alive. If you want to see this setup in action, or building your own SaaS project - check it out.

October 5, 2025
Mastering Dependency Decisions in Software Projects

Mastering Dependency Decisions in Software Projects

We all want to develop features quickly. We don't want to redevelop a solution to a problem that has already been solved and battle-tested by someone else. So we often turn to an existing package - you know, browsing NPM!Adding an external package is a double-edged sword, as it comes with its own risks as well - extra bundle size, security vulnerabilities, breaking changes, etc.In order to tackle this dependency management, I will show you exactly:- How you can assess the necessity of a new package- What are the best practices- How you can set rules in your team- How you can make sure rules are followedHow Do You Keep Your Dependencies Under Control?First step to successfully managing dependencies is *drum rolls* - not to have any! But that doesn't happen often, does it? That would require us to reinvent everything and we don't want to do that. Avoiding dependencies entirely would require us to reinvent everything, and we don't want to do that. So the second best thing is to find a way how to assess, which dependency we will add to our project and which not.Let’s look at the best practices for deciding whether a particular dependency is needed in our project.Best Practices For Evaluating the Need For a New DependencyNecessity of the DependencyWe need to assess whether dependency is truly necessary. We can do this by asking ourselves following questions:Ease of implementation - Can the functionality be implemented reasonably, without adding external package? Ask your peers, senior members of the team, or even ChatGPT!Native solution - Do native solutions exists? Whether it is in your existing codebase, existing library or current native browser features. Remember to also check capabilities of frameworks you use.Evaluate the need - Dependencies should add significant value or solve complex problems, not just address easily solved issues in-house.Quality And Long-Term ViabilityOnce you decide a dependency is needed, it's crucial to ensure it meets a certain level of quality and offers long-term reliability. The last thing you want is to add a new dependency that becomes unsupported shortly after, forcing you to replace it. So how do you quantify quality and long-term viability?Active Maintenance - this one is easy to spot, and most of us are already doing it. When was the latest version of the package published? What about previous versions. Are they released regularly? Weekly downloads? Most of this data is already visible on NPM page of the specific package. Community Support - Is it only creators of the package who contribute to new features and maintenance? Or are other developers also contributing? Check GitHub page of the package and "Issues" section. Look at open and closed issues, to understand how responsive the maintainers are. Remember that active user community is a strong indicator of package reliability and longevity.Up-to-date Documentation - last but not least is documentation. Is it comprehensive, clear and up-to-date? Usually, these are signs of well-maintained package. Clear installation instructions, usage guides, examples and API references are must have.License ComplianceIt's easy to install a package and just use it. But you need to be aware that it can have legal implications. Even more so, when your software is used for commercial purposes. Therefore, checking a license of a package is essential before adding it to your project. Here is the brief list of most common incenses. (Disclaimer: do your own license research, for your particular case)MIT License (details) - most permissive and commonly used. It allows you to do almost anything with the code (including using it in commercial projects) as long as the original license and copyright notice are included with any substantial portions of the software. This is commonly used for JavaScript libraries.ISC License (details) - similar to MIT license, it is permissive free software license. It allows for commercial use, modification, distribution and private use. Requires including the full text of license in modified software.Apache License 2.0 (details) - also similar to MIT license in it's permissiveness. Although, requires modified versions to state the changes made, when distributing software.Proprietary Licenses - some packages might be under proprietary licenses, where the copyright holder maintains control over the use and distribution of the software. These often come with more restrictions, especially for commercial use.It's good practice to consult with legal counsel when incorporating open-source software into commercial projects, especially if you're dealing with a variety of licenses or large codebases.SecurityThere is no such a thing as bulletproof package. So when you decide to add a new one to your code, you are opening yourself to potential vulnerabilities. Therefore it's crucial to assess the current state of the dependency. Here is how you can do it:Vulnerability Scanning - if you are using NPM package manager you can run command npm audit, which asks for report of known vulnerabilities of your packages, and if any are found, then follow steps which can be taken to fix those. Alternatively, you can use more comprehensive tools such as Snyk.Dependency Pinning - if you know the package well, and you are happy with it as it is, you can also pin a version of package to avoid automatically updating to newer versions, which might introduce new vulnerabilities. However, this needs to be balanced with the need to update for security patches. Regular Updates - many security vulnerabilities are fixed in newer versions. Therefore regularly updating is important. So if your repository is on GitHub, you can take advantage of Dependabot and configure it to check your dependencies regularly, and make pull requests for any new versions.Checking Deprecated Functions - ensure that dependency does not use deprecated or unsafe functions, which can be removed in future releases, or are not maintained anymore.Automating The Best PracticesHaving rules and guidelines is a great first step. But how do you make sure they are followed? How do you do it with as little overhead as possible?.github/CODEOWNERSIn order to be aware what dependencies are being added or removed, you can specify a person or a team members, which will need to approve any changes in regards to dependencies in your project, such as any changes in your package.json file.In your root of the repository create a folder called .github and inside of it a file called CODEOWNERS. Here you can specify a rules you want. For example if you want to require a specific team member approval for any changes in package.json file you can do the following:// Inside .github/CODEOWNERS**/package.json @usernameThis rule will apply to all package.json files in your repo. Approval of @username will be required for PR to be merged.DependabotIn order to automate dependencies with Dependabot, we need to configure it. We do this by creating root of our project, inside .github folder a dependabot.yml file.# dependabot.yml configuration fileversion: 2updates: # Package manager to be used - package-ecosystem: "npm" # Look through all directories directory: "/" schedule: # daily | weekly | monthly interval: "weekly" open-pull-requests-limit: 10 ignore: # For all packages, ignore all patch updates - dependancy-name: "*" update-types: ["version-update:semver-patch"]With this configuration file, we use npm as package manager. Dependabot looks through all directories and check package updates on weekly basis. It will open maximum 10 pull requests at a time. If you have a big project and you didn't have Dependabot before, I suggest setting a rule of ignoring "patch" versions on dependencies in the beginning so you can focus on major and minor versions. And once you have all dependencies up to date, you can remove that rule. As often patch versions contains bugfixes and security patches.BundlephobiaIf you want to find out performance impact of your npm packages and it's effect on your bundle size or see historical trends, then this tool is for you. You can either use it online, by searching for specific package name, or you can upload your package.json file.License ScanningAs I already mentioned, you need to be aware of the licenses associated with the packages you use. You can utilize a tool like FOSSA to help you protect your software against license violations. Additionally, you can achieve continuous compliance by integrating it into your CI pipeline.ConclusionManaging project dependencies in a lean and clean manner is essential for efficient development of software. While leveraging external libraries can accelerate feature development, it's crucial to navigate this path with a strategic approach. By assessing the necessity, quality, and long-term viability of each dependency, ensuring compliance with licensing, and maintaining robust security protocols, you can significantly mitigate the risks of added dependencies.Embracing best practices, setting clear team rules, and utilizing tools for automation and monitoring are key steps to maintaining a healthy dependency ecosystem. Remember, the goal isn't just to add features rapidly but to build sustainable, secure, and efficient software that stands the test of time.

June 12, 2025
Imperative vs Reactive Programming

Imperative vs Reactive Programming

OverviewReactive and imperative programming are two different programming paradigms with distinct approaches to managing and executing code.Both imperative and reactive programming have their own advantages and disadvantages, and the choice between the two depends on the particular problem that is being solved. Let's dive in!Imperative ProgrammingImperative programming - focuses on describing a sequence of steps that the computer must follow in order to solve a problem. It is centered around the use of statements that change a program's state, and the programmer specifies the exact order in which these statements should be executed. The programming style is characterized by its focus on describing how a program should work, rather than what it should do.Imperative programming is best used in scenarios where you want to have complete control over the flow of a program and where you want to specify the exact steps that need to be taken to accomplish a task. This makes it ideal for implementing simple algorithms, procedures, or scripts that need to be executed in a specific order.Some examples of when you might use imperative programming include:- Implementing simple utility scripts that perform specific tasks, such as converting files from one format to another.- Implementing procedural algorithms that have a clear set of steps to follow, such as a sorting algorithm.- Implementing lower-level system components, such as drivers or firmware, that need to perform specific operations in a specific order.- Implementing game logic, where the order of events and interactions is critical to the player experience.In general, imperative programming is a good choice when you want to have a high degree of control over the flow of a program and when the solution to a problem can be easily expressed as a sequence of steps.Why did the imperative programmer cross the road? To get to the other side of the code and change the state!Reactive ProgrammingReactive programming focuses on designing systems that respond to changes in data over time. It is centered around the use of reactive data streams, which are sequences of events that can be processed as they occur. The programming style is characterized by its focus on designing systems that react to changes in data, rather than specifying how a program should work.Reactive programming is best used in scenarios where you want to react to changes in data over time, or when you want to create applications that are highly responsive, scalable, and maintainable.Some examples of when you might use reactive programming include:- Developing real-time applications, such as games, chat systems, or financial dashboards, that need to respond quickly to user interactions and changes in data.- Developing event-driven systems, such as web services or microservices, that need to handle large amounts of data and events in a scalable and efficient manner.Implementing user interfaces, where the state of the UI is driven by user interactions and changes in data.- Developing systems that handle multiple data streams, such as video and audio, and need to process and respond to these streams in real-time.In general, reactive programming is a good choice when you want to build applications that are highly responsive, scalable, and maintainable, and when you want to react to changes in data over time.Let's cook some omelette!Cooking - Imperative WayIn simple terms, you can think of imperative programming like being a head-chef in a kitchen, giving strict instructions to your other chefs on exactly how to cook each dish. You tell your chefs exactly what ingredients to use, in what order, and how much of each to use.Here's an example:function make_omelette() { crack_eggs(); whisk_eggs(); heat_pan(); add_butter(); pour_eggs(); put_ham(); flip_omelette(); put_on_plate(); serve_omelette();}:make_omelette();It's like you're giving a set of commands, one after another, to get the desired outcome: a delicious omelette!Imperative programming is great when you want to have complete control over every step of the process, but it can get quite tedious when the process gets more complicated. Plus, if one of your chefs makes a mistake, you have to go back and fix it yourself!Cooking - Reactive WayUsing the same example as above, instead of giving each chef strict instructions, you simply set up the kitchen with all ingredients and equipment and let chefs do their thing.Here is a JavaScript example:import { Observable } from "rxjs";function make_omelette() { const ingredients = new Observable(subscriber => { subscriber.next("eggs"); subscriber.next("cheese"); subscriber.next("ham"); subscriber.next("butter"); subscriber.complete(); }); const eggsChef = ingredients.filter(ingredient => ingredient === "eggs"); const cheeseChef = ingredients.filter(ingredient => ingredient === "cheese"); const hamChef = ingredients.filter(ingredient => ingredient === "ham"); const butterChef = ingredients.filter(ingredient => ingredient === "butter"); const omelette = Observable.zip(eggsChef, cheeseChef, hamChef, butterChef, (e, c, h, b) => "Cheesy Ham Omelette"); omelette.subscribe(dinner => console.log("Dinner is ready:", dinner));}make_omelette();In this example, the head-chef sets up the kitchen by creating an observable stream of ingredients. Each chef filters the stream to get the ingredients they need for their part of the dish. The head chef then combines the dishes into the final product: a delicious cheesy ham omelette! The best part is, with reactive programming, if one ingredient is delayed or changes, the omelette updates automatically. No need for the head-chef to intervene!ConclusionIn conclusion, both imperative and reactive programming are powerful tools for solving different kinds of problems, and the choice between the two depends on the specific requirements of the problem at hand. By understanding the strengths and weaknesses of each approach, YOU as a developer can make informed decisions about the best programming paradigm to use for their particular use case.

April 4, 2025

Read more on Linkedin