top of page

Angular 19 Standalone Architecture Migration

  • 2 days ago
  • 4 min read

person at computer working

Hey kids! Is anyone else tired of messing up where to add the Module versus component in the declarations versus imports? No? Just me? Oof. Anyways .....


gif

I have been using Angular since Angular 2. No, I didn't forget a number, I meant 2. Just 1 number ... and it was not fun to use back in that time, it was clunky and difficult to use, but I could see the potential. I liked where it was trying to go, but it wasn't there yet ... I was lucky enough to be able to walk away from it for awhile until about version 8-ish ... and it was much more mature and easier to work with then. It still had a ton of dependencies that made it difficult to split off components for reusability in projects as singular objects, so it was easier to create suites and inject libraries for reuse. Not terrible. Then, the stars aligned and in version 17 standalone components became available as a default! This was only a few years ago as of this writing so many projects have been written with coupled components and modules that would benefit from being split apart and migrated to standalone components.



What is the difference between the traditional approach and the standalone approach?


Traditional NgModule Approach


In traditional Angular, every component needs a home - it must be declared in an NgModule. The component itself doesn't know what it needs. The NgModule "owns" the component and provides all its dependencies.


// Components declare dependencies via NgModule
@NgModule({
  declarations: [AppComponent, MenuComponent],  // Components owned by this module
  imports: [MyModule, RouterModule],            // Other modules we need
  providers: [AuthService],                     // Services available
  bootstrap: [AppComponent]
})
export class NewModule { }

// Component doesn't know its own dependencies
@Component({
  selector: 'new-root',
  standalone: false,  // Needs to be in an NgModule
})
export class NewComponent { }

Standalone Approach


With standalone, each component declares its own dependencies.


// Component declares its own dependencies directly
@Component({
  selector: 'new-root',
  standalone: true,  // or leave line null - it's now the default
  imports: [OneModule, RouterItem, MenuButtonComponent, MenuHeaderComponent],  // Exactly what THIS component needs
})
export class NewComponent { }

// No NgModule needed! Bootstrap directly:
bootstrapApplication(NewComponent, {
  providers: [/* services and config */]
});

Key Benefits


  • Self-contained: Each component explicitly lists only what it needs, so there's no guessing what a module upstream is secretly providing for you.

  • Better tree-shaking: Unused dependencies won't be bundled, because the compiler can actually see exactly what each component uses.

  • Simpler mental model: No "module scope" to track. What you see in the component is what it needs. Full stop.

  • No NgModule required: Components can be used directly without needing to wire up a module first.

Module Federation friendly: Sharing components across micro frontends gets a LOT easier when they're not dragging a module along with them.



Hopefully that gives enough to see why this is worth doing. For more information, a decent blog post on it is here.


I used the standalone-migration process and command found on the Standalone Migration page (this is for version 19, adjust for the version you need on the site) on the official Angular site, with the migration code here on Github (this isn't needed, but it's nice to have), to assist in some of the basic migration that can be automated. Now, my project was for work and I cannot share the exact code, but I can share general information regarding what steps I took.

You run the same command 3 times, but each run does a different job and the order matters.

// run at root, choose 'Convert declarations to standalone', build - run - commit
npx nx generate @angular/core:standalone

// run at root, choose 'Remove unnecessary NgModules', build - run - commit
npx nx generate @angular/core:standalone

// run at root, choose 'Switch to standalone bootstrapping API', build - run - commit
npx nx generate @angular/core:standalone

The first pass converts your declarations to standalone components. The second cleans up the NgModules that are now empty or unnecessary after that first pass. The third switches your bootstrapping over to the standalone API. Build, run, and commit between each one so if something breaks, you know exactly which step caused it. Trust me on the commits. You don't want to be untangling all three passes at once if something goes sideways.


gif

You may have some residual declarations and / or modules that will need to be manually removed. You will definitely have a ton of unit tests that need updated, and that's where a lot of the manual work lives. The declaration / import listings will change and most declarations will be empty or removed (your preference). The big shift is that declarations basically go away in tests too. Your component IS the import now. Any shared dependencies it needs get imported directly instead of being pulled in through a module.


// example.component.spec.ts (Before)
beforeEach(async () => {
  await TestBed.configureTestingModule({
    // Component is declared here
    declarations: [ ExampleComponent ],
    // Dependencies are brought in via their parent modules
    imports: [ CommonModule, SharedModule ] 
  }).compileComponents();
});

// example.component.spec.ts (After)
beforeEach(async () => {
  await TestBed.configureTestingModule({
    // Component is now moved to imports
    imports: [ ExampleComponent ], 
    // declarations: [] is often empty for standalone component tests
    providers: [
      // Use new standalone provider APIs if needed
      provideHttpClient() 
    ]
  }).compileComponents();
});

The mocks usually need to be addressed too, to validate the mocks are being used and not the actual component in the tests. Linting will find these errors and usually gives a detailed enough message that it shows exactly which import is missing. Run your tests however you need to, based on the framework used: Jasmine and Karma, Jest. You add the import listed in the error message, remove the module declaration, move on.


thumbs up

Now that AI is here and pretty darn useful, you can also have your coding assistant work through these issues for you pretty quickly too!!



Once you have your site running and your tests are passing, you now have a working site again and your components can be used individually if you want to!!



Happy coding!! 💜






Comments


bottom of page