Starting fresh with Angular

Starting fresh with Angular

A new Angular project gives you a rare luxury: no legacy decisions to work around. It also gives you an excellent opportunity to invent problems you do not have yet. Before the first useful screen exists, you can already have a shared library, a state framework and a folder structure that needs a presentation.

I prefer a different starting point. Build one real feature all the way through, including its failures. Let that feature expose the decisions the project actually needs. Here is how I would approach a greenfield Angular application with a .NET API in 2026.

Pick one useful journey

Consider a customer portal where users can view invoices. The first slice is not “set up the frontend”. It is: an authenticated customer can see their invoices, open one and understand what happens if it cannot be loaded.

That gives us something concrete to discuss with a product owner. Which invoices belong to this customer? Which fields should be visible? Can several employees access the same organisation? What happens after their access is removed?

Those questions shape the API and the interface together. An empty table backed by a fake array will not force them into the conversation.

Define failure before the spinner

For this first slice I want distinct states for loading, no invoices, a failed request and a successful result. An empty list after an error tells the user something false. A spinner that never ends tells them nothing.

I also want a direct URL to an invoice to work after a refresh. That small requirement tests routing, deployment configuration, session handling and data loading. It is a useful amount of reality for a first feature.

Start with the framework

I would use a supported Angular release, its CLI and standalone components as the starting point. Keep the generated setup understandable, commit the lockfile and agree on the Node version used locally and in CI. Make the first production build early.

I would not spend the first day replacing every default. A team needs a reason to add another build abstraction or test runner, especially when everybody will have to maintain that decision.

The Angular release policy matters here: supported versions and a routine for updates are more valuable than announcing that the application used the newest version on the day it was created. Budget for staying current.

Organise by the work

I prefer feature folders over one enormous collection of components and services. The Angular style guide also recommends organising around feature areas. A small starting structure could look like this:

src/app/
  app.config.ts
  app.routes.ts
  invoices/
    invoices.routes.ts
    invoice-list.ts
    invoice-detail.ts
    invoice-api.ts
    invoice.ts
  account/
    account.routes.ts
    account-page.ts
  shared/
    ui/
      loading-state.ts

This is a sketch, not a naming law. The important part is that changing the invoice feature does not require exploring six unrelated top-level folders. Keep something local until sharing it solves a real second use case.

A component used in one feature does not become reusable because we put it in shared. It becomes a dependency that other features can now accidentally rely on.

Give state a home

Not all state belongs in the same place. A filter that should survive sharing a link belongs in the URL. Temporary selection can live in a component. Data needed across several screens may justify a feature service. The API remains responsible for persisted business data.

For local reactive values and derived state, Signals are a good fit. Computed signals let you describe a value derived from other signals without manually keeping a second copy up to date.

import { computed, signal } from '@angular/core';

type Invoice = {
  id: string;
  status: 'paid' | 'unpaid';
};

const invoices = signal<Invoice[]>([]);
const showUnpaidOnly = signal(false);

const visibleInvoices = computed(() =>
  showUnpaidOnly()
    ? invoices().filter(invoice => invoice.status === 'unpaid')
    : invoices()
);

The example deliberately contains no HTTP request or global store. It demonstrates one responsibility: deriving the visible list. If the filter becomes part of navigation, the URL should become its source of truth instead of maintaining two competing values.

Keep RxJS where it helps

I would still use RxJS for asynchronous sequences where its operators express the behaviour clearly: user input, request cancellation and combining streams. Signals and RxJS solve overlapping but different problems; choosing one for local state does not require banning the other.

A search box is a useful example. Debouncing input, ignoring repeated values and switching to the latest request can be a clear observable pipeline. Be careful what cancellation means: stopping an HTTP subscription does not undo a server-side operation that has already happened.

A global state library can be justified by complex shared workflows, debugging needs or established team conventions. It should answer an actual problem. Installing one before there is any state worth discussing is architecture by shopping list.

Agree on the API

With a .NET backend, I want an explicit contract for the first feature. Define identifiers, dates, nullable fields, pagination and error responses. A field called date is not a contract if one developer means a calendar date and another means a UTC timestamp.

ASP.NET Core's OpenAPI support can help describe the API and support generated clients. That reduces duplicated typing, but generated TypeScript types do not validate arbitrary runtime responses. The deployed API and the generated client still need to match.

Keep transport details behind a small feature API layer. A page should not need to know how every HTTP error body is structured. Equally, do not hide meaningful distinctions such as unauthenticated, forbidden and temporarily unavailable behind one generic “something went wrong”.

Decide who is allowed

The browser can decide which controls are useful to display. It cannot be trusted to decide which invoices a user may read. The backend must enforce that rule for each relevant operation.

Write the rule down while designing the endpoint, then test it through the real API. A customer changing an invoice ID must not gain access to another customer's data. I work through that example in Security by design in Angular.

Make the form ordinary

Forms deserve an early decision because they spread through a business application quickly. I would start with a consistent approach that the team can explain, using typed reactive forms where their explicit model fits. Angular's reactive forms documentation is a useful baseline.

Define when validation appears, where server errors go and what happens during submission. Labels, keyboard access and understandable messages are part of the feature. They are not polish reserved for the final sprint.

Disabling a submit button helps prevent accidental repeat clicks. It does not make a business operation safe to repeat. If duplicate requests can cause duplicate charges or orders, the server needs a deliberate strategy too.

A design system starts small

Agree on spacing, typography, focus styling and a few controls the first feature needs. Use semantic HTML wherever possible. Add a component abstraction when it removes meaningful duplication or protects a useful convention.

I do not need a universal form engine to ship a customer settings page. I do need an error message that a user can find and understand.

Put the first slice in CI

From the first feature, CI should produce a production build and run checks that can reject a broken change. I would separate fast feedback from more expensive integration work so that one slow task does not hide an immediate compilation error.

  • Compile and check the frontend, including templates and agreed lint rules.
  • Test important behaviour in components and feature logic.
  • Run API integration tests for data access and authorisation.
  • Exercise one complete invoice journey in a browser against a test environment.
  • Check dependencies, secrets and source code with tools suited to the repository.

For the invoice list, test the empty state, a failed request and the unpaid filter. For access control, test a second user. A coverage percentage cannot choose those cases for you.

The build should be deployable through the real process. Verify route refreshes, configuration, useful error logging and rollback while the application is still small. These are much easier to reason about before six teams depend on the environment.

Leave room to learn

After the first slice, review what hurt. Was data loading duplicated? Did the API contract keep changing? Was a shared component genuinely useful? Now you have evidence for the next architectural decision.

Write down the important choices briefly: the decision, why it fits and what would make you reconsider it. Future developers need that context more than another diagram of boxes labelled “service”.

A greenfield project is successful when the next useful feature is straightforward to build and safe to change. It is not successful because the first commit contains every pattern the team has ever heard of.

That is the kind of groundwork I like to help teams establish as a freelance Angular and .NET consultant: enough structure to work confidently, with decisions grounded in the product rather than in an imaginary future.

Comments

There are no comments yet, leave yours below.

Leave a comment

Do you have an addition, question or experience related to this article? Share it below.

Comments are briefly reviewed before they appear.

Read more about:

Security by design in Angular

Security by design in Angular

A route guard and a green pipeline do not stop one customer reading another customer's invoice. Using Angular and a .NET API, I turn a security requirement into server-side ownership checks and tests. Security by design starts with boundaries and evidence, before choosing a scanner.

Continue reading

CIAM: keep your tokens in memory

A successful CIAM login is only part of the story. What does your frontend keep afterwards? I explain the user and transaction stores in oidc-client-ts, implement a TypeScript memory store and cover the trade-offs around redirects, reloads and SSO. In-memory tokens reduce persistence; they do not make an application immune to malicious JavaScript.

Continue reading

The bullshit of frontend development

The bullshit of frontend development

Frontend tooling was supposed to help us deliver. So why do straightforward websites turn into months of work? Drawing on my experience, I challenge unnecessary frameworks, custom abstractions and premature optimisation, and ask where accessibility and our users went in the process.

Continue reading