Skip to content

MEAN Stack Interview Questions for 3 Years Experience

40 questions with answers for candidates with 3 years of experience — 6 easy, 29 medium, 5 hard.

What interviewers expect: 3 years experience

With three years, you are expected to own modules end to end. Interviews include designing a feature such as notifications or approvals across Angular and Node.js, state management choices like NgRx or SignalStore, API versioning, background jobs, transactions and CI/CD. Scenario questions explore production incidents, cross-team API contracts and how you review code and support junior developers.

This set is weighted towards Angular Forms and HTTP Client (9), Services, Dependency Injection and Routing (6), MongoDB Data Modelling and Queries (5), Angular Components and Templates (5).

Module design State management Backend reliability Code reviews
Solved at this levelStart practicing
3 QUESTIONS FREESign up to unlock the full question bank and track your progress.Unlock every question and answer, then continue practising company-wise and package-wise.
3 questions free Unlock the rest in seconds
01

What are Mongoose virtuals, instance methods and statics?

Save to ▾
Show answer
Virtuals are computed properties not stored in MongoDB, such as fullName from firstName and lastName, or a virtual populate that links related documents. Instance methods are functions available on each document, like user.comparePassword(). Statics are functions on the model itself, like User.findByEmail(email). Virtuals are excluded from JSON output unless you enable toJSON: { virtuals: true } in schema options.
02

How do built-in and custom validators work in reactive forms?

Save to ▾
Show answer
Validators are functions that take a control and return null when valid or an error object like { required: true } when invalid. Built-ins include Validators.required, email, minLength, maxLength, pattern, min and max. A custom validator follows the same ValidatorFn signature, for example rejecting weak passwords. Group-level validators can compare fields, such as password and confirm password. Errors are read through control.errors and control.hasError.
03

A component subscribes to a service Observable in ngOnInit, and after navigating back and forth several times, the API is called multiple times per action. What is wrong?

Save to ▾
Show answer
The subscriptions are never cleaned up, so each visit adds another live subscriber that reacts to every emission. Use the async pipe or toSignal in the template, or add takeUntilDestroyed() to manual subscriptions. Also check that the service does not create a new long-lived stream per call and that nested subscribes are replaced with switchMap. Angular DevTools and network logs confirm the fix.
04

A form must show fields based on earlier answers, such as company name only when employment type is salaried. How would you build it?

Save to ▾
Show answer
Subscribe to the employment type control's valueChanges, or use a signal, and enable or disable the dependent controls accordingly; disabled controls are excluded from value and validation. Show the fields in the template with @if. For forms driven by configuration, build controls dynamically from a JSON schema into a FormGroup. Remember to clear stale values when a field is hidden.
05

Your app must call different API base URLs for development, staging and production. How do you configure this in Angular?

Save to ▾
Show answer
Use environment files such as environment.ts and environment.prod.ts with fileReplacements in angular.json build configurations, or load a config.json at startup with provideAppInitializer so one build works in every environment. Expose the URL through an InjectionToken and inject it in services or an interceptor. Never put secrets in these files, because everything in the Angular bundle is public.
06

Write a zod validation middleware for an Express route in TypeScript.

Save to ▾
Show answer
Reusable middleware with typed output. import { z, ZodSchema } from 'zod'; import { RequestHandler } from 'express'; export const validateBody = (schema: ZodSchema): RequestHandler => (req, res, next) => { const parsed = schema.safeParse(req.body); if (!parsed.success) { return res.status(400).json({ errors: parsed.error.flatten().fieldErrors }); } req.body = parsed.data; next(); }; const createOrder = z.object({ items: z.array(z.object({ productId: z.string(), qty: z.number().int().min(1) })).min(1), address: z.string().min(10) }); router.post('/orders', validateBody(createOrder), ordersController.create);
07

Write a debounced search helper in TypeScript using a closure.

Save to ▾
Show answer
A typed debounce keeps argument types. function debounce<A extends unknown[]>(fn: (...args: A) => void, ms = 300) { let timer: ReturnType<typeof setTimeout> | undefined; return (...args: A) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), ms); }; } const search = debounce((term: string) => console.log('search', term), 400); search('ang'); In Angular you would usually use the RxJS debounceTime operator instead.
08

Write a cart service using signals with computed totals.

Save to ▾
Show answer
Expose read-only state and mutation methods. @Injectable({ providedIn: 'root' }) export class CartService { private items = signal<CartItem[]>([]); readonly cart = this.items.asReadonly(); readonly count = computed(() => this.items().reduce((n, i) => n + i.qty, 0)); readonly total = computed(() => this.items().reduce((s, i) => s + i.qty * i.price, 0)); add(product: Product) { this.items.update((list) => { const found = list.find((i) => i.id === product.id); return found ? list.map((i) => (i.id === product.id ? { ...i, qty: i.qty + 1 } : i)) : [...list, { ...product, qty: 1 }]; }); } remove(id: string) { this.items.update((l) => l.filter((i) => i.id !== id)); } }
09

What is Angular SSR and why would you enable it?

Save to ▾
Show answer
Angular server-side rendering renders pages to HTML on a Node.js server, so users and search engines see content immediately. The browser then hydrates the page, attaching Angular to the existing DOM; incremental hydration can hydrate parts only when needed using @defer triggers. Route-level render modes allow some pages to be prerendered at build time and others rendered per request. It improves SEO and first load for public pages.
10

What is a route resolver and when would you use one?

Save to ▾
Show answer
A resolver fetches data before a route activates, so the component receives ready data instead of showing an empty state first. A functional resolver, ResolveFn<Product>, uses inject() to call a service and returns an Observable or Promise. The data is available through ActivatedRoute or bound inputs. Use it for detail pages that are useless without data, but avoid it for slow calls, because navigation waits.
11

Serve the built Angular app and the API from one Express server.

Save to ▾
Show answer
API first, static files next, fallback last. import express from 'express'; import path from 'node:path'; const app = express(); app.use(express.json()); app.use('/api', apiRouter); const dist = path.join(process.cwd(), 'dist/shop/browser'); app.use(express.static(dist, { maxAge: '1y', index: false })); app.get('/*splat', (req, res) => { res.sendFile(path.join(dist, 'index.html')); }); app.listen(process.env.PORT || 3000); The named wildcard follows Express 5 path syntax.
12

What are @defer blocks and how do they help performance?

Save to ▾
Show answer
@defer lets you lazy load part of a template and its dependencies only when a trigger occurs, such as on viewport, on interaction, on hover, on idle, on timer or a custom condition with when. Angular automatically splits that code into a separate chunk. You can show @placeholder, @loading and @error blocks. It is ideal for heavy below-the-fold widgets like charts, comments and maps.
13

What are directives in Angular and what types exist?

Save to ▾
Show answer
Directives add behaviour to elements. Components are directives with templates. Attribute directives change appearance or behaviour of an existing element, such as ngClass, ngStyle or a custom appHighlight using host bindings. Structural directives change the DOM layout by adding or removing elements, like the older *ngIf and *ngFor, now largely replaced by built-in control flow. Custom directives keep repeated DOM logic out of components.
14

How do you test an Angular service that makes HTTP calls?

Save to ▾
Show answer
Provide provideHttpClient() and provideHttpClientTesting() in TestBed, then inject the service and HttpTestingController. Call the service method and subscribe, use httpMock.expectOne('/api/products') to capture the request, assert its method and body, and flush a fake response. Finally call httpMock.verify() to ensure no unexpected requests remain. This tests the service logic without a real backend.
15

What testing tools are used in Angular projects today?

Save to ▾
Show answer
Angular's CLI has moved to Vitest as the default unit test runner for new projects, replacing the deprecated Karma, while many existing projects still run Karma with Jasmine or have switched to Jest. TestBed configures components and services for tests, HttpTestingController mocks HTTP calls, and component harnesses from the CDK simplify interaction tests. Playwright or Cypress handle end-to-end tests.
16

An order form lets users add any number of items with product, quantity and price. How do you build it?

Save to ▾
Show answer
Use a FormArray of FormGroups, each with product, quantity and price controls and validators like min(1). Provide addItem and removeItem methods that push or removeAt on the array, and render rows with @for over items.controls. Calculate the total from valueChanges or a computed signal. On submit, send getRawValue() typed as the order DTO, and disable the submit button while invalid or pending.
17

Tell me about a complex form you built and the challenges you solved.

Save to ▾
Show answer
Situation: I built a loan application form with five steps and over sixty fields. Task: make it reliable and user friendly. Action: I used typed reactive forms split into step groups, cross-field validators for income rules, a FormArray for co-applicants, auto-saved drafts every thirty seconds and mapped server validation errors back to fields. Result: drop-offs reduced noticeably and QA found very few form bugs.
18

Write a role-based canMatch guard that only matches admin users.

Save to ▾
Show answer
Guard factories keep it reusable. import { inject } from '@angular/core'; import { CanMatchFn, Router } from '@angular/router'; export const roleGuard = (role: string): CanMatchFn => () => { const auth = inject(AuthService); const router = inject(Router); return auth.user()?.role === role ? true : router.createUrlTree(['/forbidden']); }; export const routes = [ { path: 'admin', canMatch: [roleGuard('admin')], loadChildren: () => import('./admin/admin.routes').then(m => m.ADMIN_ROUTES) } ];
19

What are pipes in Angular and what is the difference between pure and impure pipes?

Save to ▾
Show answer
Pipes transform values in templates, such as {{ price | currency:'INR' }}, date, uppercase, json and async. A pure pipe, the default, runs only when its input reference or primitive value changes, so it is very efficient. An impure pipe runs on every change detection cycle, which can hurt performance, and is needed only when output depends on mutable data. Prefer pure pipes or computed signals.
20

OTP codes must automatically expire ten minutes after creation. How would you implement this with MongoDB?

Save to ▾
Show answer
Store each OTP in its own collection with a createdAt date field and create a TTL index, db.otps.createIndex({ createdAt: 1 }, { expireAfterSeconds: 600 }). MongoDB's background task removes expired documents automatically, though deletion can lag by about a minute, so also check the timestamp during verification. Store a hash of the code rather than plain text, and limit verification attempts.
21

Write a functional HTTP interceptor that adds a JWT and handles 401 errors.

Save to ▾
Show answer
Register with provideHttpClient(withInterceptors([authInterceptor])). export const authInterceptor: HttpInterceptorFn = (req, next) => { const auth = inject(AuthService); const router = inject(Router); const token = auth.token(); const authReq = token ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) : req; return next(authReq).pipe( catchError((err: HttpErrorResponse) => { if (err.status === 401) { auth.logout(); router.navigate(['/login']); } return throwError(() => err); }) ); };
22

How do you validate request data in an Express API, and why is it necessary even with Angular validation?

Save to ▾
Show answer
Frontend validation improves user experience, but anyone can call the API directly with tools like Postman or curl, so the server must validate everything. Use a schema library such as zod, Joi or express-validator in middleware to check types, required fields, lengths and formats, reject unknown fields and return 400 with clear field errors. Mongoose validation is a second layer close to the data.
23

Provide a configuration object through an InjectionToken and use it in a service.

Save to ▾
Show answer
Tokens carry non-class values. import { InjectionToken, inject, Injectable } from '@angular/core'; export interface AppConfig { apiUrl: string; pageSize: number } export const APP_CONFIG = new InjectionToken<AppConfig>('APP_CONFIG'); // app.config.ts providers: [{ provide: APP_CONFIG, useValue: { apiUrl: environment.apiUrl, pageSize: 20 } }] @Injectable({ providedIn: 'root' }) export class OrderService { private config = inject(APP_CONFIG); private http = inject(HttpClient); list() { return this.http.get(`${this.config.apiUrl}/orders`, { params: { limit: this.config.pageSize } }); } }
24

Build a dynamic FormArray for adding and removing phone numbers.

Save to ▾
Show answer
Expose the array and helper methods. export class ContactFormComponent { private fb = inject(NonNullableFormBuilder); form = this.fb.group({ name: ['', Validators.required], phones: this.fb.array([this.fb.control('', Validators.pattern(/^\d{10}$/))]) }); get phones() { return this.form.controls.phones; } addPhone() { this.phones.push(this.fb.control('', Validators.pattern(/^\d{10}$/))); } removePhone(i: number) { this.phones.removeAt(i); } } // <div formArrayName='phones'> // @for (c of phones.controls; track $index) { // <input [formControlName]='$index'> <button type='button' (click)='removePhone($index)'>x</button> // } // </div>
25

How does lazy loading work with the Angular router?

Save to ▾
Show answer
Lazy loading defers downloading a feature's code until the user navigates to it. Use loadComponent: () => import('./admin/admin.component').then(m => m.AdminComponent) for a single component, or loadChildren to load a file of child routes. The CLI builds a separate chunk for each lazy route. You can add preloading strategies like withPreloading(PreloadAllModules) to fetch lazy chunks in the background after startup.
26

How does change detection work in Angular, and what does OnPush do?

Save to ▾
Show answer
Change detection checks component bindings and updates the DOM when data changes. Traditionally Zone.js patched async APIs and triggered a check of the whole tree after events, timers and HTTP calls. With ChangeDetectionStrategy.OnPush, a component is checked only when an input reference changes, an event fires inside it, an async pipe emits or a signal it reads changes. OnPush with immutable data greatly reduces work.
27

Load three APIs in parallel with forkJoin and handle a failure in one of them.

Save to ▾
Show answer
Each request gets its own fallback. import { forkJoin, of, catchError } from 'rxjs'; loadDashboard() { return forkJoin({ sales: this.api.getSales().pipe(catchError(() => of(null))), orders: this.api.getRecentOrders().pipe(catchError(() => of([]))), users: this.api.getUserStats().pipe(catchError(() => of(null))) }); } dashboard = toSignal(this.loadDashboard()); // template: @if (dashboard(); as d) { ... d.sales ... } forkJoin emits once, after every inner Observable completes.
28

What is the difference between valueChanges and statusChanges, and what does updateOn do?

Save to ▾
Show answer
valueChanges is an Observable that emits the new value whenever a control or group changes, useful for dependent fields or type-ahead search. statusChanges emits when validation status changes between VALID, INVALID, PENDING and DISABLED. The updateOn option controls when value and validation update: change by default, blur to validate when the field loses focus, or submit to validate only on form submission.
29

Write a generic TypeScript function that groups an array of items by a key.

Save to ▾
Show answer
Generics keep the result strongly typed. function groupBy<T, K extends keyof T>(items: T[], key: K): Record<string, T[]> { return items.reduce((acc, item) => { const group = String(item[key]); (acc[group] ||= []).push(item); return acc; }, {} as Record<string, T[]>); } interface Order { id: number; status: string } const byStatus = groupBy<Order, 'status'>(orders, 'status'); K extends keyof T ensures only real property names are accepted.
30

What are FormControl, FormGroup, FormArray and FormBuilder?

Save to ▾
Show answer
FormControl tracks the value and validation state of a single input. FormGroup groups named controls into an object, such as a registration form. FormArray holds a dynamic list of controls or groups, such as multiple phone numbers or order items. FormBuilder, usually NonNullableFormBuilder, is a helper service that creates these with shorter syntax, for example fb.group({ email: ['', Validators.required] }).
31

What are route guards and what kinds exist in modern Angular?

Save to ▾
Show answer
Guards decide whether navigation can proceed. canActivate checks before entering a route, canActivateChild covers child routes, canDeactivate runs before leaving, often to warn about unsaved changes, and canMatch decides whether a route definition even matches, useful for role-based lazy loading. Modern guards are plain functions using inject(), returning true, false, a UrlTree for redirect, or an Observable or Promise of these.
32

What schema design patterns are common in MongoDB?

Save to ▾
Show answer
Common patterns include embedding for one-to-few relations, referencing for one-to-many and many-to-many, the subset pattern that embeds only the most recent items, the extended reference pattern that copies frequently needed fields like a customer name into orders, the computed pattern that stores precalculated totals, the bucket pattern that groups time-series readings, and the outlier pattern for rare documents with huge arrays.
33

Write an aggregation that finds the top five customers by total spend with their names.

Save to ▾
Show answer
Group, sort, limit, then join. db.orders.aggregate([ { $match: { status: 'paid' } }, { $group: { _id: '$customerId', spend: { $sum: '$total' }, orders: { $sum: 1 } } }, { $sort: { spend: -1 } }, { $limit: 5 }, { $lookup: { from: 'users', localField: '_id', foreignField: '_id', as: 'customer' } }, { $unwind: '$customer' }, { $project: { _id: 0, name: '$customer.name', spend: 1, orders: 1 } } ]) Joining after $limit means only five lookups run.
34

Your API needs to send welcome emails and SMS after signup, but this makes signup slow. What would you change?

Save to ▾
Show answer
Do not make the user wait for third-party providers. After saving the user, publish an event or add jobs to a queue and return the response immediately. Workers send the email and SMS with retries and backoff, and failures are logged without affecting signup. An EventEmitter is enough for a quick in-process version, but a persistent queue survives restarts and scales across instances.
35

Write a unit test for an Angular component using TestBed.

Save to ▾
Show answer
Test what the user sees. import { TestBed } from '@angular/core/testing'; import { CounterComponent } from './counter.component'; describe('CounterComponent', () => { it('increments when the button is clicked', async () => { const fixture = TestBed.createComponent(CounterComponent); await fixture.whenStable(); const el: HTMLElement = fixture.nativeElement; el.querySelector('button')!.click(); await fixture.whenStable(); expect(el.querySelector('p')!.textContent).toContain('Count: 1'); }); }); Standalone components need no testing module declarations.
36

What are signals in Angular and why were they introduced?

Save to ▾
Show answer
A signal is a reactive value container: you read it by calling it, count(), and update it with set or update. Angular tracks which templates, computed values and effects read a signal, so when it changes only dependent parts update. Signals give fine-grained reactivity, enable zoneless change detection and are simpler than RxJS for synchronous component state. They are stable and central to modern Angular.
37

What are typed reactive forms?

Save to ▾
Show answer
Since Angular 14, reactive forms are strongly typed. A FormControl<string> knows its value type, so form.value and getRawValue return typed objects and the compiler catches wrong field names. Controls are nullable by default because reset sets them to null; using nonNullable: true or NonNullableFormBuilder resets to the initial value instead. Typed forms make it safe to map form values directly to API request interfaces.
38

Define a typed Mongoose model in TypeScript with an instance method.

Save to ▾
Show answer
Interface, methods type and schema together. import { Schema, model, Model } from 'mongoose'; import bcrypt from 'bcrypt'; interface IUser { name: string; email: string; password: string; role: 'user' | 'admin' } interface IUserMethods { comparePassword(plain: string): Promise<boolean> } type UserModel = Model<IUser, {}, IUserMethods>; const userSchema = new Schema<IUser, UserModel, IUserMethods>({ name: { type: String, required: true }, email: { type: String, required: true, unique: true, lowercase: true }, password: { type: String, required: true, select: false }, role: { type: String, enum: ['user', 'admin'], default: 'user' } }, { timestamps: true }); userSchema.method('comparePassword', function (plain: string) { return bcrypt.compare(plain, this.password); }); export const User = model<IUser, UserModel>('User', userSchema);
39

Write an exhaustive switch over a union type using never.

Save to ▾
Show answer
The never check makes the compiler flag unhandled cases. type Shape = { kind: 'circle'; r: number } | { kind: 'square'; side: number }; function area(s: Shape): number { switch (s.kind) { case 'circle': return Math.PI * s.r ** 2; case 'square': return s.side ** 2; default: { const unreachable: never = s; throw new Error(`Unknown shape ${unreachable}`); } } } Adding a triangle variant now causes a compile error until it is handled.
40

List the Angular CLI commands you use most often in a project.

Save to ▾
Show answer
Common commands: ng new shop-app --style=scss ng serve --open ng generate component features/product-list ng generate service core/auth ng generate guard core/auth --functional ng generate pipe shared/truncate ng build --configuration production ng test ng update @angular/core @angular/cli ng update applies official migrations automatically, which makes version upgrades much easier.

Frequently asked questions

What is asked in MEAN Stack interviews for 3 years of experience?

With three years, you are expected to own modules end to end. Interviews include designing a feature such as notifications or approvals across Angular and Node.js, state management choices like NgRx or SignalStore, API versioning, background jobs, transactions and CI/CD. Scenario questions explore production incidents, cross-team API contracts and how you review code and support junior developers.

How many questions should I practise before a MEAN Stack interview?

Work through all 40 questions here, then the company-wise sets for the employers you are targeting. Quality matters more than count: be able to explain why each answer is right.

Which topics matter most for 3 years of experience?

Focus first on Module design, State management, Backend reliability, Code reviews.