Angular interviews for freshers check that you can explain the basics in your own words, write a small component on a whiteboard and talk honestly about what you built. Expect questions on Angular versus AngularJS, TypeScript and decorators, the CLI and project files, template bindings, pipes, services and HttpClient, a first router setup, and the errors every beginner hits. This page is written for final-year students, new graduates and anyone heading into a first Angular job from an internship or a bootcamp. Read each sample answer, then say your own version out loud. Change detection, dependency injection in depth, RxJS operators and guards are on the main Angular page.
Search all questions by round, difficulty and level, or save the ones you want to practice.
| AngularJS | the original 1.x framework, written in plain JavaScript, built around controllers and a scope object. |
|---|---|
| Angular | version 2 onward, a complete rewrite in TypeScript built around components. |
Not compatible: you can't just upgrade; moving means rewriting or running both side by side for a while.
Today: AngularJS is no longer supported, so new work uses Angular.
“They're related, but they're really two different frameworks. AngularJS is the original version, the 1.x line. It was plain JavaScript, and you organised code with controllers and a scope object, with two-way binding checked by a digest loop. Angular, from version 2 onward, was rewritten from scratch. It uses TypeScript, the app is built from components, it has a proper dependency injection system and a CLI, and it compiles templates ahead of time so the app starts faster. Because it was a rewrite, an AngularJS app can't be upgraded just by bumping the version. Teams either rewrite it or run both together for a while and move screens across one by one. AngularJS isn't supported anymore, so anything new I'd build is in Angular.”
Treating Angular as simply AngularJS version 2, or describing controllers and a scope object as the way modern Angular apps are built.
Types: you declare what a variable, parameter or API response looks like, and mistakes show up before you run the app.
Editor help: autocomplete and safe renames, because the editor knows the shapes.
Compiled away: the build turns TypeScript into plain JavaScript; the browser never sees the types.
Angular uses it: decorators, interfaces for data, and type checks in templates.
“TypeScript is JavaScript with types added on top. I can say a function takes a number, or describe the shape of a user object with an interface, and if I pass the wrong thing the compiler tells me straight away instead of the app breaking in the browser. That also makes the editor much smarter, so I get autocomplete on my own objects and can rename things safely. The browser never runs TypeScript. The Angular build compiles it down to normal JavaScript, and the types disappear. In my project it saved me when the API renamed a field: I updated the interface and the compiler pointed at every place that used the old name. Angular also leans on TypeScript for decorators like @Component and for checking bindings in templates.”
interface User {
id: number;
name: string;
email?: string; // optional
}
function greet(user: User): string {
return `Hi, ${user.name}`;
}
greet({ id: 1, name: 'Asha' }); // fine
// greet({ id: 1 }); // compile error: name is missing
Saying the browser runs TypeScript, or that types check data at run time when they vanish after compiling.
What it is: a function, written with @, that attaches extra information to a class or property.
@Component: marks a class as a component and gives its selector, template, styles and imports.
@Injectable: marks a class as a service that Angular's injector can create and hand out.
Others: @Input, @Output and @HostListener decorate properties and methods.
“A decorator is a TypeScript feature: a function written with an at sign above a class, property or method, which attaches extra information to it. On its own, a component class is just a normal class with some fields and methods. The @Component decorator is what tells Angular it's a component, and it carries the metadata: the selector tag, the template or template file, the styles, and in a standalone component the imports it uses. @Injectable does the same job for services. It tells Angular's injector it can create this class, and with providedIn root there's one shared instance. There are property decorators too, like @Input for data coming in from a parent. Newer Angular also offers functions like input() in place of some decorators, but the idea of describing the class to Angular stays the same.”
@Component({
selector: 'app-greeting',
standalone: true,
template: `<h2>Hello, {{ name }}</h2>`,
styles: [`h2 { color: teal; }`],
})
export class GreetingComponent {
@Input() name = 'guest';
}
// Used in a parent template: <app-greeting name="Maya" />
Saying a decorator is just a comment that does nothing, or not knowing what the selector is for.
One page: the browser loads one HTML file and the app's JavaScript once.
Client-side routing: the router swaps components and updates the URL with no full reload.
Upsides: fast, app-like navigation and only data sent after the first load.
Downsides: heavier first load, search engines see less without server rendering, the server must return index.html for every route.
“In a single-page application the browser loads one HTML page and the JavaScript for the app once. After that, when I click a link, Angular's router swaps the component on screen and updates the address bar, but the browser doesn't fetch a whole new page. Only the data comes from the server, usually as JSON. That makes navigation feel quick, like an app. The downsides are real, though. The first load can be heavier because there's more JavaScript up front, which is why lazy loading matters. Search engines and link previews may see an almost empty page unless you add server-side rendering. And the server has to return index.html for any route, or refreshing on a deep link breaks. For a mostly static content site, a multi-page site can be simpler.”
Saying an SPA can only have one screen, or claiming it has no downsides at all.
What it is: a command-line tool that creates, runs, tests and builds Angular projects.
Create and run: ng new makes the project; ng serve runs a dev server that reloads on save.
Generate: ng generate component (short form ng g c) creates the files with consistent names.
Ship: ng build makes the optimised files for deployment; ng test runs unit tests.
“The Angular CLI is the command-line tool I use for almost everything in a project. I start with ng new and a project name, which sets up the folder, the TypeScript config, the build setup and a first component. Then ng serve starts a local dev server, and every time I save a file the page reloads. When I need a new piece, I run ng generate component, or the short form ng g c, with a name, and it creates the TypeScript, template, styles and test file with consistent naming. There are generators for services, pipes and guards too. When I'm ready to deploy, ng build produces optimised JavaScript and CSS in the dist folder, and ng test runs the unit tests. Using the CLI keeps the project laid out like other Angular projects, which helps anyone joining.”
Never having run the CLI, or thinking the ng serve dev server is what you deploy to a real server.
index.html: the single page; it holds the root tag, usually <app-root>.
main.ts: the entry point; it bootstraps the root component with the app config.
App config and routes: app-wide providers such as the router and HttpClient, plus the routes list.
angular.json and package.json: build and serve settings; dependencies and scripts.
“The browser loads index.html, which is almost empty apart from an app-root tag. main.ts is the entry point. In a standalone project it calls bootstrapApplication with the root component and the app config, and Angular renders that component where the app-root tag sits. The app config file lists app-wide providers, like provideRouter with the routes file, and provideHttpClient if I'm calling APIs. The root component itself has a TypeScript class, a template and styles. Outside src, angular.json holds the workspace settings: how to build and serve, and where global styles and assets live. package.json lists the dependencies and npm scripts, and tsconfig holds the TypeScript settings. Older projects have an AppModule instead of the app config, but the idea is the same: something tells Angular which component to start with and what the whole app needs.”
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err));
Not knowing where the app starts, or thinking each route is a separate HTML file.
Cause: ngModel lives in FormsModule, and the component hasn't imported it.
Fix: add FormsModule to the standalone component's imports, or to the NgModule that declares it.
What [( )] means: a property binding plus an event binding, [ngModel] and (ngModelChange) together.
General rule: an unknown property or element error usually means a missing import.
“That error means Angular doesn't know what ngModel is in this template. ngModel isn't built into every component; it comes from FormsModule. In a standalone component I fix it by adding FormsModule to the component's imports array, and in an older module-based app I'd add it to the NgModule that declares the component. The same kind of error shows up for any directive or child component I forget to import, so it's the first thing I check. It's also worth knowing what the syntax means. Square brackets inside round brackets, which people call banana in a box, is shorthand. It's a property binding that pushes my value into the input, plus an event binding on ngModelChange that writes the new value back to my field. So data flows both ways.”
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-name-box',
standalone: true,
imports: [FormsModule],
template: `
<input [(ngModel)]="name" />
<p>Hello, {{ name }}</p>
`,
})
export class NameBoxComponent {
name = '';
}
Trying random fixes, or not knowing that ngModel comes from FormsModule and has to be imported.
Loop: @for over the array, with track on a unique id.
Empty case: an @empty block shows the message.
Format: the currency pipe for the price.
Older syntax: *ngFor plus a separate *ngIf did the same job.
“I'd use the @for block. It loops over the products array and needs a track expression, so I track by the product id. That tells Angular which row is which when the list changes, so it only updates the rows that changed instead of rebuilding everything. Inside, I show the name and the price with the currency pipe so it's formatted properly. Then @for has an @empty block, which is perfect here: if the array has no items, it shows 'No products yet' instead. In older Angular code you'd see star ngFor on the list item and a separate star ngIf for the empty message, with trackBy as an optional function. The new syntax is built into the template language, so there's nothing to import for the loop itself.”
@Component({
selector: 'app-product-list',
standalone: true,
imports: [CurrencyPipe],
template: `
<ul>
@for (p of products; track p.id) {
<li>{{ p.name }} - {{ p.price | currency: 'EUR' }}</li>
} @empty {
<li>No products yet</li>
}
</ul>
`,
})
export class ProductListComponent {
products = [
{ id: 1, name: 'Notebook', price: 4.5 },
{ id: 2, name: 'Pen', price: 1.2 },
];
}
Not being able to write a loop in a template, or having no idea why the track expression is there.
State: store the selected tab's id in the component.
Click: (click) sets that id.
Class binding: [class.active] is true only for the matching tab.
Many classes: [ngClass] or [class] with an object when several classes depend on state.
“The Angular way is to keep the selection as data, not to touch the DOM. I store selectedId in the component, and each tab has a click binding that sets selectedId to that tab's id. Then on the tab I use a class binding, class.active, set to whether this tab's id matches selectedId. Angular adds the active class when that's true and removes it when it's false, so only one tab is ever highlighted, and the CSS for active handles the look. If several classes depend on state, I'd use ngClass with an object, where each key is a class name and each value is a condition, or bind class to the same kind of object. What I avoid is document.querySelector and classList, because then the screen and my data can drift apart.”
@Component({
selector: 'app-tabs',
standalone: true,
template: `
@for (tab of tabs; track tab.id) {
<button [class.active]="tab.id === selectedId" (click)="selectedId = tab.id">
{{ tab.label }}
</button>
}
`,
styles: [`.active { font-weight: bold; border-bottom: 2px solid; }`],
})
export class TabsComponent {
tabs = [
{ id: 1, label: 'Home' },
{ id: 2, label: 'Orders' },
{ id: 3, label: 'Profile' },
];
selectedId = 1;
}
Reaching for document.querySelector and classList inside an Angular component.
What it is: #name on an element gives the template a handle to that element, or to a component on a component tag.
Use: pass box.value into a method on click or on Enter.
Scope: it lives in that template only; the class needs a view query to see it.
When not: for validation or several fields, use a form instead.
“A template reference variable is a name I give an element with a hash sign, like hash box on an input. Anywhere else in the same template I can refer to box, and for a plain element it's the actual DOM element, so box.value is whatever the user typed. For a quick todo input I don't need forms at all. I bind keyup.enter and a button click to an add method, pass box.value, and then clear the input by setting its value to an empty string. The variable only lives in the template, so the class doesn't see it unless I use a view query. On a component tag, the variable points to the component instance instead of an element. For anything with validation or several fields, I'd switch to a real form.”
@Component({
selector: 'app-todos',
standalone: true,
template: `
<input #box placeholder="New task" (keyup.enter)="add(box.value); box.value = ''" />
<button (click)="add(box.value); box.value = ''">Add</button>
<ul>
@for (t of todos; track $index) {
<li>{{ t }}</li>
}
</ul>
`,
})
export class TodosComponent {
todos: string[] = [];
add(text: string) {
if (text.trim()) this.todos.push(text.trim());
}
}
Using document.getElementById to read the value, or thinking the reference variable is automatically available in the class.
ng-container: a grouping element that renders nothing itself, so you can wrap content without adding an extra div.
ng-template: a block of template that Angular keeps aside and doesn't render until something asks for it.
Rendering it: ngTemplateOutlet stamps the block out where you want it, with data passed through a context object.
The star: *ngIf and *ngFor are shorthand for Angular wrapping the element in an ng-template.
“Both are elements that don't show up in the final HTML. ng-container is just a grouping box. If I need to wrap some elements, but an extra div would break a table or a flex layout, I use ng-container and Angular renders only the children. In older code it's also how you'd use star ngIf and star ngFor on the same content, since one element can't take two structural directives. ng-template is different: its content isn't rendered at all until something asks for it. I use it for a block that repeats, like a person's name and role that I show in two places. I write it once, give it a reference name, and stamp it out with ngTemplateOutlet, passing the data in through a context object. And the star in star ngIf is really shorthand: Angular wraps that element in an ng-template behind the scenes.”
@Component({
selector: 'app-team',
standalone: true,
imports: [NgTemplateOutlet],
template: `
<ng-template #person let-p>
<strong>{{ p.name }}</strong> - {{ p.role }}
</ng-template>
<h3>Lead</h3>
<ng-container *ngTemplateOutlet="person; context: { $implicit: lead }"></ng-container>
<h3>Members</h3>
@for (m of members; track m.name) {
<p><ng-container *ngTemplateOutlet="person; context: { $implicit: m }"></ng-container></p>
}
`,
})
export class TeamComponent {
lead = { name: 'Sam', role: 'Design' };
members = [{ name: 'Lina', role: 'Front end' }, { name: 'Omar', role: 'Testing' }];
}
Thinking ng-container adds a real element to the page, or expecting ng-template content to appear on its own.
State: a count held in a signal or a plain field.
Events: (click) on each button calls a method.
Rule in the class: the decrease method enforces the lower limit, so the template stays simple.
Extra touch: disable the minus button at zero so the user sees the limit.
“I'd keep the count in a signal starting at zero. The template shows it by calling count() inside double curly braces, and each button has a click binding that calls a method. The increase method just adds one with update. The decrease method is where the rule lives: it subtracts one but uses Math.max with zero, so the value can't go negative. I keep that check in the class rather than in the template, because logic in templates is harder to read and test. As a small extra, I bind the disabled property of the minus button to count() being zero, so the user can see they can't go lower. If the interviewer prefers, the same thing works with a plain number field instead of a signal, because the click event makes Angular update the screen anyway.”
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<button (click)="decrease()" [disabled]="count() === 0">-</button>
<span>{{ count() }}</span>
<button (click)="increase()">+</button>
`,
})
export class CounterComponent {
count = signal(0);
increase() {
this.count.update((c) => c + 1);
}
decrease() {
this.count.update((c) => Math.max(0, c - 1));
}
}
Putting all the logic inline in the template, or forgetting to call the signal with brackets when reading it.
ng-content: marks where the parent's content is placed inside the child's template.
Several slots: select with an attribute or tag sends different pieces to different places.
Inputs vs content: inputs for simple values, projection for markup.
Owned by the parent: projected content keeps the parent's bindings and data.
“This is content projection. In the card's template I put the frame, the border and padding, and an ng-content tag where the body should go. Whatever the parent writes between the opening and closing app-card tags lands there. If I want a separate title area, I add a second ng-content with select, for example matching an attribute called card-title, and the parent marks its heading with that attribute. Anything without a match goes into the plain ng-content. I use inputs for simple values like a title string, but for markup such as a button or an image, projection is cleaner than adding an input for every option. The projected content still belongs to the parent, so its bindings use the parent's data and click handlers.”
@Component({
selector: 'app-card',
standalone: true,
template: `
<div class="card">
<header><ng-content select="[card-title]" /></header>
<section><ng-content /></section>
</div>
`,
styles: [`.card { border: 1px solid #ddd; border-radius: 8px; padding: 16px; }`],
})
export class CardComponent {}
// Parent template:
// <app-card>
// <h3 card-title>Order summary</h3>
// <p>Three items, arriving Friday.</p>
// </app-card>
Adding a dozen string inputs to fake flexibility, or not knowing ng-content exists.
Emulated by default: Angular adds a generated attribute to the component's elements and rewrites its selectors to require it.
Global styles: the global stylesheet listed in angular.json applies everywhere.
:host: styles the component's own tag.
Other modes: ViewEncapsulation.None makes styles global; ShadowDom uses the browser's shadow DOM.
“By default Angular uses emulated view encapsulation. When it renders a component, it adds a generated attribute to that component's elements, and it rewrites the component's CSS so each selector also requires that attribute. So my .title rule really becomes .title plus that component's attribute, and it can't match elements in another component. If I want something to apply to the whole app, like fonts or a reset, I put it in the global styles file. To style the component's own tag, I use the :host selector. The flip side is that my styles don't reach into child components either, which surprises beginners. There's ViewEncapsulation.None, which makes a component's styles global, and ShadowDom, which uses the browser's real shadow DOM, but I'd pick those on purpose, not as a quick fix.”
Thinking all component CSS is global, or switching to ViewEncapsulation.None whenever a style doesn't apply.
Timing: normal view queries are set once the view is created, which is after ngOnInit.
Fix: use ngAfterViewInit, or static: true if the element is never inside an @if or @for.
Conditional elements: inside @if the element may not exist until the condition is true.
Newer API: the viewChild() function gives a signal that updates when the element appears.
“@ViewChild is a view query, and the view isn't finished when ngOnInit runs. Angular sets normal view queries once it has created the component's view, and the hook for that is ngAfterViewInit, so I'd move the focus call there and use the ElementRef's nativeElement to call focus. If the input is always in the template, never inside an @if or a loop, I can mark the query static true, and then it's already set in ngOnInit. If it's inside an @if, it may not exist at all until the condition turns true, so I'd focus it after that happens. In newer Angular I'd use the viewChild function, which gives a signal, so reading it later always gives the current element or undefined. What I wouldn't do is wrap it in a setTimeout without knowing why that seems to work.”
@Component({
selector: 'app-search',
standalone: true,
template: `<input #searchBox placeholder="Search" />`,
})
export class SearchComponent implements AfterViewInit {
@ViewChild('searchBox') searchBox!: ElementRef<HTMLInputElement>;
ngAfterViewInit() {
this.searchBox.nativeElement.focus();
}
}
Saying @ViewChild is broken, or hiding the timing problem with a setTimeout and no explanation.
What it is: a small function used in a template with | to change how a value is shown.
Built-ins: date, currency, decimal, percent, uppercase, titlecase, json and async.
Parameters and chaining: arguments go after a colon, and pipes can be chained left to right.
Import: in a standalone component, import each pipe you use, or CommonModule.
“A pipe transforms a value for display in the template, using the vertical bar. The data in my component stays as it is; only what's shown changes. For a date, I'd use the date pipe with a format, like day, short month and year. For a price, the currency pipe with a currency code, and it adds the right symbol and decimal places. For a name, titlecase or uppercase. Pipes take parameters after a colon and can be chained, so I could lowercase a value and then slice it. The json pipe is handy for debugging, to dump an object on screen, and the async pipe subscribes to an observable for me. In a standalone component I have to import the pipes I use, like DatePipe, or CommonModule to get all the common ones.”
@Component({
selector: 'app-order-row',
standalone: true,
imports: [DatePipe, CurrencyPipe, TitleCasePipe],
template: `
<p>{{ customer | titlecase }}</p>
<p>Placed on {{ placedAt | date: 'dd MMM yyyy' }}</p>
<p>Total {{ total | currency: 'EUR' }}</p>
`,
})
export class OrderRowComponent {
customer = 'maria lopez';
placedAt = new Date();
total = 42.5;
}
Changing the stored data itself just to format it for display, or calling a method in the template for every format.
Class: @Pipe with a name, implementing PipeTransform.
transform: takes the value plus optional parameters and returns the display value.
Edge cases: empty or null text, and text already shorter than the limit.
Use: import it into the component, then {{ text | truncate: 30 }}.
“I'd generate it with ng g pipe truncate, which gives me a class with the @Pipe decorator and the name truncate. It implements PipeTransform, so it has a transform method. The first argument is the value from the left of the bar, and extra arguments come after colons, so I take the text and a limit with a default of twenty. First I handle the edge cases: if the text is null or empty I return an empty string, and if it's already short enough I return it unchanged. Otherwise I slice it to the limit, trim any trailing space and add three dots. Any component that wants it imports TruncatePipe and writes the text, a bar, truncate, a colon and thirty. Because it's a pure pipe, it only reruns when the text or the limit changes.”
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'truncate', standalone: true })
export class TruncatePipe implements PipeTransform {
transform(text: string | null | undefined, limit = 20): string {
if (!text) return '';
if (text.length <= limit) return text;
return text.slice(0, limit).trimEnd() + '...';
}
}
// In a template: {{ product.description | truncate: 30 }}
Crashing on null text, or not knowing how the parameters after the colon reach the transform method.
Component's job: show data and react to the user.
Service's job: fetch and save data, hold shared state, apply business rules.
Reuse: two components share one service instead of copying code.
Testing: you can swap the service for a fake in a component test.
“A component should mostly be about the screen: showing data and handling what the user does. If I put the HTTP call, the URL and the data mapping inside the component, the next component that needs the same data copies that code, and when the API changes I have to fix it in several places. So I move it into a service, say ProductService, with a method like getProducts. Components ask Angular for that service through dependency injection and just call the method. That also makes testing easier, because in a component test I can give it a fake service that returns fixed data, with no real network. And a service can hold shared state, like the items in a cart, so two unrelated components see the same cart.”
@Injectable({ providedIn: 'root' })
export class ProductService {
private http = inject(HttpClient);
getProducts() {
return this.http.get<Product[]>('/api/products');
}
}
// In a component
export class ProductPageComponent {
private productService = inject(ProductService);
products$ = this.productService.getProducts();
}
Saying services are only for HTTP, or that it's fine to copy the same fetch code into every component.
Setup: provideHttpClient() in the app config, then inject HttpClient or a service.
Three states: loading, data and error, each shown with @if.
Subscribe: next stores the data, error sets a friendly message; both turn loading off.
Typed data: an interface for the user shape.
“First, the app config needs provideHttpClient, otherwise injecting HttpClient fails. In the component I keep three signals: the users, a loading flag that starts true, and an error message. I use signals so the template updates reliably when the response arrives. In ngOnInit I call get with the User type, so the result is typed, and subscribe with an object. In next, I set the users and turn loading off. In error, I set a friendly message like 'Could not load users' and also turn loading off, because a spinner that never stops is a common beginner bug. The template uses @if blocks for loading and error, and a loop for the list. In a real app I'd move the call into a UserService, and since HttpClient completes after one response, this subscription ends by itself.”
interface User { id: number; name: string; }
@Component({
selector: 'app-users',
standalone: true,
template: `
@if (loading()) { <p>Loading...</p> }
@if (error()) { <p class="error">{{ error() }}</p> }
<ul>
@for (u of users(); track u.id) { <li>{{ u.name }}</li> }
</ul>
`,
})
export class UsersComponent implements OnInit {
private http = inject(HttpClient);
users = signal<User[]>([]);
loading = signal(true);
error = signal('');
ngOnInit() {
this.http.get<User[]>('/api/users').subscribe({
next: (data) => { this.users.set(data); this.loading.set(false); },
error: () => { this.error.set('Could not load users.'); this.loading.set(false); },
});
}
}
Showing only the happy path, or leaving the loading message on screen forever when the request fails.
Why: different ports are different origins, so the browser blocks the response unless the server allows it.
Real fix: the backend sends CORS headers allowing the app's origin.
Dev shortcut: the Angular CLI proxy forwards /api calls, so the browser only sees one origin.
Not the fix: adding headers to the request from Angular.
“CORS isn't an Angular error; it's the browser protecting the user. An origin is the scheme, host and port, so localhost 4200 and localhost 3000 count as different origins. The request may even reach the server, but the browser won't hand the response to my code unless the response includes a header saying my origin is allowed. So the proper fix is on the backend: enable CORS for the front end's origin, for example with a CORS middleware in a Node server. For local development there's also the Angular CLI proxy. I add a proxy config file that forwards anything under /api to localhost 3000, point the serve settings at it, and call relative URLs like /api/users. Then the browser only ever talks to 4200. Adding headers in an Angular interceptor won't fix it.”
Calling CORS an Angular problem, or suggesting a browser extension that turns off the check as the fix.
Routes: an array of path and component pairs, given to provideRouter.
Outlet and links: <router-outlet> marks where pages appear; routerLink navigates without a reload.
Parameter: :id in the path, read with ActivatedRoute or received as a component input.
Wildcard last: ** catches unknown URLs, so it goes at the end.
“I'd write a routes array. An empty path goes to HomeComponent, users slash colon id goes to UserDetailComponent, and double star goes to NotFoundComponent. The wildcard has to be last, because the router takes the first match. I pass the routes to provideRouter in the app config. In the root template I put router-outlet where pages should appear, and my links use routerLink instead of href, so clicking doesn't reload the whole app. In the detail component I inject ActivatedRoute and read the id from paramMap. The snapshot is fine if the component is created fresh each time, but if the user can go from user 5 to user 6 on the same page, the component is reused, so I'd subscribe to paramMap instead. Newer apps can also turn on component input binding and receive the id as an input.”
export const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'users/:id', component: UserDetailComponent },
{ path: '**', component: NotFoundComponent },
];
// user-detail.component.ts
export class UserDetailComponent {
private route = inject(ActivatedRoute);
id = this.route.snapshot.paramMap.get('id'); // always a string, like '42'
}
Using plain href links that reload the whole app, or putting the wildcard route first.
What and who: the app, who it was for, and your part if it was a team.
Structure: main pages, key components, and which services held the data.
One decision: something you chose and why, such as a shared component or a service.
Honest lesson: what you'd change now.
“In my final year I built a library booking app with two classmates. Students could search books, reserve one and see their loans, and the librarian had an admin page. I did most of the Angular side, and the backend was a small Node API. I had routes for search, book details, my loans and admin. The search page had a search bar component and a list of book cards, and I reused the card on the loans page with an input that switched its button between reserve and return. All the HTTP calls went through a BookService and an AuthService, so components never built URLs themselves. One thing I'd change is that I put too much logic in the search component early on and had to pull it into the service later. Next time I'd start with the service.”
Describing features without being able to say how the code was organised, or claiming a team project was all yours.
The problem: what you saw, in one or two sentences.
How you searched: the error message, the console and Network tab, small experiments.
Cause and fix: what it turned out to be.
What you learned: a habit you kept.
“During my internship I added a new page with a form, and the whole page came up blank with an error in the console I didn't understand, something about no provider for a service. I spent a while changing random imports, which didn't help, so I stopped and read the message properly. It named the exact service and the component asking for it. I opened the service and saw I'd dropped providedIn root while copying code from another file, so Angular didn't know how to create it. Adding it back fixed the page. What I took from it is to read the full error before touching code, and to check the console and Network tab first. Now when I'm stuck I also write down what I've tried, which makes it easier to ask a senior for help.”
A story where the fix was luck or someone else simply solved it, with nothing learned.
Find it: run the app, find the page, search the codebase for the form's component.
Follow the pattern: see how other fields are built, validated and sent to the API, and match that style.
Check the edges: does the backend accept the field, and what format and rules are expected?
Ask and test: confirm unclear rules early, test the change, keep the pull request small.
“First I'd get the app running locally and find the form in the browser. Then I'd search the codebase for a label or field name I can see on screen, which usually takes me straight to the component. Before writing anything, I'd look at how the existing fields are done: is it a reactive form, how validators are added, how errors are shown, and how the data reaches the API through a service. I'd copy that pattern rather than doing it my own way. I'd also check whether the backend already accepts a phone field, and ask my mentor about the rules, like country codes and whether it's required, instead of guessing. Then I'd test it by hand, add or update a unit test if the project has them, and keep the change small for review.”
Rewriting the form in your own style, or staying stuck for days without asking anyone.
ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your live interview audio and screen are never stored. Your resume and notes are saved to your account so the app fills them in on any computer. It stays out of screen share on every plan, including Free; only you can see it.