Telerik blogs

Learn how to properly structure a large Blazor web application with real-world proven tips and tricks.

Organizing large Blazor web applications is crucial.

Everyone who has ever worked in a large code base knows how a properly structured application feels much simpler and more natural to navigate than a big chunk of code smashed together.

In this article, I will share general tips and a few Blazor-specific ideas. All those ideas are tested in real-world Blazor web applications of different sizes.

What Is a ‘Large’ Blazor Web Application?

Let’s start by defining what a “large” Blazor web application is.

Real-world projects always grow over time. We add new pages and components, bring in more developers to the project, and the application state becomes increasingly complex.

Sooner or later, we will:

  • Ask ourselves, “Where does this component live?”
  • See duplicated logic across several pages
  • Find bugs harder and harder to understand and resolve

That’s where a Blazor web application turns into a “large” application.

In this article, I want to share a few strategies and ideas to get you move past that point and help you with structuring large applications in a way that they don’t feel as hard to navigate and work with.

Group By Feature, Not By Technicality

One of the most fundamental things I do in every project is organizing the code by feature and not by technicality.

Instead of the default project folders:

  • Pages
  • Components
  • Services
  • Models

I prefer a feature-based structure like this:

  • Orders/
    • OrdersPage.razor
    • OrderDetailsPage.razor
    • Models/
      • Order.cs
    • Services/
      • OrderService.cs

Keeping the code close together lets me see the whole picture of the feature and navigate between the moving parts more quickly.

New developers will have an easier time adapting to new features or getting to know the codebase. You open a folder, and they instantly see everything that’s important for their scope.

Also, changes within the same feature are limited to a specific area of the project, making it a lot simpler for code reviews and working with different change sets (Git merging).

On the other hand, splitting the code into several technical folders makes it harder to navigate, and I find myself constantly looking for files and components.

Render Components vs. Behavioral Components

Separating render components (presentational, without logic) from behavioral components that handle data fetching and manage state helps reduce complexity.

Consider a page that shows a list of orders. There are maybe different types of orders. For example, online orders, over-the-phone orders and walk-in orders.

We create four components:

  • OrderList.razor
  • OnlineOrder.razor
  • OverThePhoneOrder.razor
  • WalkInOrder.razor

We want three dedicated components rendering the detailed information for each order type. However, we do not want those components to become overly complex or difficult to maintain.

Instead, we implement the data fetching in an OrderList component and provide the data as a parameter to the three dedicated render components used as children.

Bonus: If we want to take things a step further and we see reusability in such render components, we might extract them into a separate, shared class library project.

State Management in Larger Blazor Applications

The first tip is to keep the state as close to the feature as possible whenever possible. If the state is only used in a component, keep it there.

The next step up is implementing simple state containers or introducing Flux-like patterns. Learn more about complex state management in Blazor.

It’s important to keep a single source of truth and make it as manageable as possible. Sometimes, you are not sure what works best and have to figure out what works best for your project and your team.

In general, I found that using Flux-like patterns is an all-or-nothing decision. Do not try to apply it only to a part of the application. It makes it more complex and difficult to maintain.

Whenever possible, I try to use local component state and scoped services injected via dependency injection.

Splitting into Multiple Projects

I have yet to work in an application where I felt the need to extract features into a dedicated project.

However, most of it comes down to how you work on your application. I’m used to working in small teams, often on my own. When multiple developers or teams work on the same application, it can be helpful to extract the features maintained by a given team into a dedicated project.

You might think that basing your application architecture on team structure is bad advice. Generally speaking, I agree.

However, in Blazor applications, I can see scenarios where clear boundaries between teams working on different parts can be effectively communicated through dedicated projects.

The main benefits of splitting a Blazor web application into multiple projects are:

  • Clear boundaries between projects and responsibilities
  • Better reusability across different applications
  • Simple project structure (compared to everything in one giant project)

Generally, I’d start with a single project setup and create dedicated projects only when one of the benefits above justifies the extraction.

Bonus: Document the responsibility of each project in the README.md file to communicate to all developers what code to put or where to find it in your project.

Dependency Injection Best Practices

As a Blazor application grows, the number of services registered with the dependency injection container usually increases as well.

I highly recommend grouping service registrations into extension methods, such as:

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddOrderServices(this IServiceCollection services)
    {
        services.AddScoped<IOrderService, OrderService>();

        return services;
    }
}

and use it in the Program.cs file:

services.AddOrderServices();

Using an extension method per feature provides a sweet spot between the lines of code in the Program.cs, how frequently it will be changed and how many services are grouped within a dedicated service registration extension method.

Bonus: Place the extension method implementation in the associated feature folder in the project structure. Again, keeping everything together that belongs together.

Properly Handling API & Data Access

Loading and manipulating data are central use cases in almost all Blazor web applications.

Instead of cluttering your Blazor components with injected HttpClient objects, implement services that handle data access.

Consider an OrderService that uses an injected HttpClient object to fetch data from the server. The service provides specific methods and data types to the Blazor components consuming it.

This separation not only allows for cleaner component code, but also improves (or enables) testability of the data access code. And it also helps with reusability.

Bonus: Implement an abstract base class that handles retry logic and general error handling. That way, you do not have to reinvent the wheel in every specific implementation.

Use Scalable Naming Conventions

It’s probably one of the things I first learned when getting into software development over two decades ago.

Naming is hard, but with a scalable naming convention, everything becomes much easier to find and maintain.

A few guidelines I found helpful in most projects:

  • Page suffix for main components injecting data services and managing state (e.g., OrderListPage)
  • List suffix for components rendering a reusable collection (e.g., OrderList)
  • Service suffix for services (pure logic, no Blazor component code) (e.g., OrderService)
  • Model suffix for data objects transporting data from services to Blazor components (and vice-versa) (e.g., OrderModel)

You can extend this list and customize it to fit your development style and project. However, it’s a good starting point for most Blazor projects.

Bonus: Extra points for documenting the naming conventions in the README.md file of the project to make it accessible for new and seasoned developers as a reference.

Use CSS Isolation For Blazor Components

CSS is a beautiful tool. However, its global nature leads to many conflicts. With CSS isolation, we can avoid CSS definitions clashing with each other by scoping them to a specific component.

I recommend putting global styles in an app.css file in the wwwroot folder and using Component.razor.css files to scope component-relevant CSS code.

@code Blocks vs. Code-Behind Files

I used to do a lot of WPF desktop application development back in the day. Using the MVVM pattern, separating the behavior from the presentation was critical.

With Blazor, we have the option to use @code blocks (C# code) inside the same file as the component template code (HTML). It’s my favorite approach.

However, you can also use a code-behind file to implement the behavior of a component.

Whatever strategy you prefer, I highly recommend using the same style for all components in your project, team, or company. I have had rather negative experiences working in code bases with mixed styles.

Bonus: Document your preferred approach in the project’s README.md file to make it crystal clear to all developers which style should be used.

Conclusion

Good organization eventually leads to better code because it simplifies navigation, reduces complexity and makes it feel a lot better to work in.

Do not make the mistake of creating a huge architecture at the beginning of a project. The most promising approach is to keep things simple in the beginning and let the architecture and structure evolve with the project. Introduce one thing at a time and carefully examine whether it improves the project or makes it overly complex.

It’s not possible to apply all of the ideas shown in this article at the same time. It’s also not necessary. Applying the ideas that work best for your project, depending on its state and scale, is what moves the needle.

If you want to learn more about Blazor development, watch my free Blazor Crash Course on YouTube. And stay tuned to the Telerik blog for more Blazor Basics.


About the Author

Claudio Bernasconi

Claudio Bernasconi is a passionate software engineer and content creator writing articles and running a .NET developer YouTube channel. He has more than 10 years of experience as a .NET developer and loves sharing his knowledge about Blazor and other .NET topics with the community.

Related Posts

Comments

Comments are disabled in preview mode.