Skip to content

Template method, framework hooks, and the step passed as a function

The base owns the order and calls your step; in your own code, hand the step in as a function.

Predict first

Before you read: predict

A payments ledger's back office logs its staff in through @nestjs/passport and passport-local:

A client posts {"username":"ana"} to /login, with no password field.

What status does it get?

Code for this question
// staff.strategy.tsimport { Injectable } from '@nestjs/common';import { PassportStrategy } from '@nestjs/passport';import { Strategy } from 'passport-local';import { createHash } from 'node:crypto'; const digest = (s: string): string => createHash('sha256').update(s).digest('hex');const staff: Record<string, string> = { ana: digest('ledger-pin-7') }; @Injectable()export class StaffStrategy extends PassportStrategy(Strategy) {  async validate(username: string, password: string) {    return staff[username] === digest(password) ? { username } : null;  }} // login.controller.tsimport { Controller, HttpCode, Post, Req, UseGuards } from '@nestjs/common';import { AuthGuard } from '@nestjs/passport'; @Controller()export class LoginController {  @Post('login')  @HttpCode(200)  @UseGuards(AuthGuard('local'))  login(@Req() req: { user: { username: string } }) {    return { welcome: req.user.username };  }} // app.module.tsimport { Module } from '@nestjs/common';import { LoginController } from './login.controller';import { StaffStrategy } from './staff.strategy'; @Module({ controllers: [LoginController], providers: [StaffStrategy] })export class AppModule {}
Pick one

The 30-second version

A template method is a base-class method that runs an algorithm's steps in a fixed order, leaving some to subclasses. Frameworks use the shape: passport-local reads both fields from the request and calls your validate() only when both are present; the guard answers any failed login with 401. In your own code, pass the varying step as a function.

The base owns the order and calls your step; in your own code, hand the step in as a function.

How it works

Step 1 of 12

A skeleton in the base. One base-class method, the template method, runs an algorithm's steps in a fixed order; subclasses supply the steps that vary.
In the kitchen
  • the law fixes a signing's order: check each signer's identity, read the deed aloud, the parties sign, the notary seals → the template method's fixed skeleton (step 1)

Analogy: A notary's signing appointment (legal services)

At the notary's office In code
the law fixes a signing's order: check each signer's identity, read the deed aloud, the parties sign, the notary seals the template method's fixed skeleton (step 1)
the client's lawyer must draft the deed's terms; the attestation clause has standard wording unless the lawyer writes one a required step and a hook with a default (step 2)
the client never tells the notary when to read or seal; the notary calls each party in at its turn the base calls the subclass's step (steps 3, 5)
for a note between two neighbours, they write it and sign it at the kitchen table the step passed as a function, with no base to extend (step 10)

In real terms: the code that owns the order calls yours at fixed points; whatever you write runs only when, and only if, that code reaches its point.

Breaks down when: a notary can refuse a clause she judges unlawful. A base class runs whatever step a subclass gives it.

Contrasting case: a shopping list you write and work through yourself. Nobody else owns the order, so there is nothing to extend: call your own functions.

Examples, run for real

4.1 ExamHall EXH-4860: three importers as subclasses

ExamHall's question bank took exams from CSV, QTI and Moodle files. Each importer was a subclass of one base whose importExam ran parse → validate → dedupe → save:

// exam-importer.ts
export interface Question { id: string; stem: string }
export abstract class ExamImporter {
  readonly saved: Question[] = [];
  importExam(source: string): number {
    const valid = this.parse(source).filter((q) => q.stem.trim() !== '');
    const unique = [...new Map(valid.map((q) => [q.id, q])).values()];
    this.saved.push(...unique);
    return unique.length;
  }
  protected abstract parse(source: string): Question[];
}
export class CsvImporter extends ExamImporter {
  protected parse(source: string): Question[] {
    return source.trim().split('\n').map((row) => { const [id, stem] = row.split(';'); return { id, stem }; });
  }
}
4.1 EXH-4860 csv 4 parsed → 2 saved · qti 3 parsed → 2 saved · moodle 3 parsed → 2 saved · 4 classes (1 base, 3 subclasses) · tsc no diagnostics

Each subclass wrote only parse; the base ran the same four steps in the same order for all three formats (steps 1 and 3).

4.2 MR !5160: one function, the parse step passed in

// import-exam.ts (MR !5160)
export interface Question { id: string; stem: string }
export function importExam(source: string, { parse }: { parse: (source: string) => Question[] }, saved: Question[]): number {
  const valid = parse(source).filter((q) => q.stem.trim() !== '');
  const unique = [...new Map(valid.map((q) => [q.id, q])).values()];
  saved.push(...unique);
  return unique.length;
}
// importExam(file, { parse: parseCsv }, bank)
4.2 MR !5160 importExam(source, { parse }, bank): the same questions saved for 3 of 3 formats · classes in import-exam.ts 0 · tsc no diagnostics

The function owns the order the base owned; each format hands in its parser, and no class is left to extend (step 10).

Try it

Answer first, then check. Nothing is saved.

Question 1 of 7

A video platform publishes every upload through one base class:

The four files are compiled together with strict on.

In which files does tsc report an error?

Code for this question
// publish-job.tsexport abstract class PublishJob {  run(video: string): string[] {    const steps = [this.encode(video)];    if (this.needsCaptions(video)) steps.push(`captions ${video}`);    steps.push(this.upload(video));    return steps;  }  protected abstract encode(video: string): string;  protected needsCaptions(video: string): boolean { return true; }  protected abstract upload(video: string): string;} // trailer-job.tsimport { PublishJob } from './publish-job';export class TrailerJob extends PublishJob {  protected encode(video: string): string { return `encode ${video} 1080p`; }  protected needsCaptions(video: string): boolean { return false; }} // episode-job.tsimport { PublishJob } from './publish-job';export class EpisodeJob extends PublishJob {  protected encode(video: string): string { return `encode ${video} 4k`; }  protected upload(video: string): string { return `upload ${video} to cdn`; }} // clip-job.tsimport { PublishJob } from './publish-job';export class ClipJob extends PublishJob {  protected encode(video: string): string { return `encode ${video} 720p`; }  protected needsCaptions(video: string): boolean { return false; }  protected upload(video: string): string { return `upload ${video} to edge`; }}
Pick one

Question 2 of 7

A payments ledger imports bank statements. An MR replaces the base class and its two subclasses with one function:

The callers become importAll(csv, { parse: parseCsv(';') }) and importAll(camt, { parse: parseCamt }).

Which review comment names a defect this MR introduces?

Code for this question
// before/statement.tsexport interface Line { date: string; ref: string; amount: number }export abstract class StatementImport {  importAll(raw: string): string[] {    const seen = new Set<string>();    const out: string[] = [];    for (const l of this.parse(raw)) {      const cents = this.cents(l);      if (cents === 0 || seen.has(l.ref)) continue;      seen.add(l.ref);      out.push(`${l.date} ${l.ref} ${cents}`);    }    return out;  }  protected abstract parse(raw: string): Line[];  protected cents(l: Line): number { return l.amount; }} // before/csv-import.tsimport { StatementImport, type Line } from './statement';export class CsvImport extends StatementImport {  constructor(private sep = ';') { super(); }  protected parse(raw: string): Line[] {    return raw.trim().split('\n').map((row) => { const [date, ref, amount] = row.split(this.sep); return { date, ref, amount: Number(amount) }; });  }} // before/camt-import.tsimport { StatementImport, type Line } from './statement';export class CamtImport extends StatementImport {  protected parse(raw: string): Line[] {    return JSON.parse(raw).entries.map((e: { d: string; id: string; eur: string }) => ({ date: e.d, ref: e.id, amount: Number(e.eur) }));  }  protected cents(l: Line): number { return Math.round(l.amount * 100); }} // mr/statement.tsexport interface Line { date: string; ref: string; amount: number }export function importAll(raw: string, steps: { parse: (raw: string) => Line[] }): string[] {  const seen = new Set<string>();  const out: string[] = [];  for (const l of steps.parse(raw)) {    if (l.amount === 0 || seen.has(l.ref)) continue;    seen.add(l.ref);    out.push(`${l.date} ${l.ref} ${l.amount}`);  }  return out;}export const parseCsv = (sep: string) => (raw: string): Line[] =>  raw.trim().split('\n').map((row) => { const [date, ref, amount] = row.split(sep); return { date, ref, amount: Number(amount) }; });export const parseCamt = (raw: string): Line[] =>  JSON.parse(raw).entries.map((e: { d: string; id: string; eur: string }) => ({ date: e.d, ref: e.id, amount: Number(e.eur) }));
Pick one

Question 3 of 7

A chat service logs every error through one filter:

A client sends GET /rooms/r1.

What does logged hold, and what does the client receive?

Code for this question
// log-errors.filter.tsimport { ArgumentsHost, Catch } from '@nestjs/common';import { BaseExceptionFilter } from '@nestjs/core'; export const logged: string[] = []; @Catch()export class LogErrors extends BaseExceptionFilter {  catch(exception: unknown, host: ArgumentsHost) {    logged.push(`logged: ${(exception as Error).message}`);  }} // rooms.controller.tsimport { Controller, Get, Param } from '@nestjs/common'; @Controller('rooms')export class RoomsController {  @Get(':id')  find(@Param('id') id: string) {    throw new Error(`room ${id} store down`);  }} // app.module.tsimport { Module } from '@nestjs/common';import { APP_FILTER } from '@nestjs/core';import { LogErrors } from './log-errors.filter';import { RoomsController } from './rooms.controller'; @Module({ controllers: [RoomsController], providers: [{ provide: APP_FILTER, useClass: LogErrors }] })export class AppModule {}
Pick one

Question 4 of 7

A box office opens its tills with a cashier's PIN:

Kai posts his right PIN, {"username":"kai","password":"4417"}, and Zoe posts a wrong one, {"username":"zoe","password":"0000"}, to /till/open.

What do the two requests get?

Code for this question
// cashier.strategy.tsimport { Injectable } from '@nestjs/common';import { PassportStrategy } from '@nestjs/passport';import { Strategy } from 'passport-local'; const cashiers = [{ name: 'kai', pin: '4417' }, { name: 'zoe', pin: '9021' }]; @Injectable()export class CashierStrategy extends PassportStrategy(Strategy) {  async validate(username: string, password: string) {    return cashiers.findIndex((c) => c.name === username && c.pin === password);  }} // till.controller.tsimport { Controller, HttpCode, Post, Req, UseGuards } from '@nestjs/common';import { AuthGuard } from '@nestjs/passport'; @Controller()export class TillController {  @Post('till/open')  @HttpCode(200)  @UseGuards(AuthGuard('local'))  open(@Req() req: { user: unknown }) {    return { cashier: req.user };  }} // app.module.tsimport { Module } from '@nestjs/common';import { CashierStrategy } from './cashier.strategy';import { TillController } from './till.controller'; @Module({ controllers: [TillController], providers: [CashierStrategy] })export class AppModule {}
Pick one

Question 5 of 7

The interviewer says: "ExamHall's results export used a base class with header(), line(row) and an optional footer(count) hook. Write it as one function instead: exportResults(rows, steps)." It returns an array of strings: steps.header() first, then steps.line(row) for each row in order, then steps.footer(rows.length) last when a footer is given. footer is optional; when line is missing, it throws a TypeError before any step runs. You have 15 minutes and a plain editor.

Say your test list first, then write it.

Question 6 of 7

A chat app's moderators log in with a PIN:

Every moderator login, right PIN or wrong, gets 500.

Which change makes a right PIN get 200 and a wrong one 401?

Code for this question
// moderator.strategy.tsimport { Injectable } from '@nestjs/common';import { PassportStrategy } from '@nestjs/passport';import { Strategy } from 'passport-local'; const pins: Record<string, string> = { mod1: '5150' }; @Injectable()export class ModeratorStrategy extends PassportStrategy(Strategy) {  async validate(username: string, password: string) {    return pins[username] === password ? { username } : null;  }} // moderation.controller.tsimport { Controller, HttpCode, Post, Req, UseGuards } from '@nestjs/common';import { AuthGuard } from '@nestjs/passport'; @Controller('moderation')export class ModerationController {  @Post('login')  @HttpCode(200)  @UseGuards(AuthGuard('local'))  login(@Req() req: { user: { username: string } }) {    return { moderator: req.user.username };  }} // app.module.tsimport { Module } from '@nestjs/common';import { ModerationController } from './moderation.controller'; @Module({ controllers: [ModerationController] })export class AppModule {}
Pick one

Question 7 of 7

A video platform logs every error with a filter that hands each one back to Nest:

setup(app) runs right after NestFactory.create.

Which argument to new LogErrors(…) makes GET /videos/v9/manifest log logged: no renditions for v9 and answer 500 {"statusCode":500,"message":"Internal server error"}?

Code for this question
// log-errors.filter.tsimport { ArgumentsHost, Catch } from '@nestjs/common';import { BaseExceptionFilter } from '@nestjs/core'; export const logged: string[] = []; @Catch()export class LogErrors extends BaseExceptionFilter {  catch(exception: unknown, host: ArgumentsHost) {    logged.push(`logged: ${(exception as Error).message}`);    super.catch(exception, host);  }} // videos.controller.tsimport { Controller, Get, Param } from '@nestjs/common'; @Controller('videos')export class VideosController {  @Get(':id/manifest')  manifest(@Param('id') id: string) {    throw new Error(`no renditions for ${id}`);  }} // app.module.tsimport { Module } from '@nestjs/common';import { VideosController } from './videos.controller'; @Module({ controllers: [VideosController] })export class AppModule {} // main.tsimport { INestApplication } from '@nestjs/common';import { HttpAdapterHost } from '@nestjs/core';import { LogErrors } from './log-errors.filter'; export function setup(app: INestApplication): void {  // app.useGlobalFilters(new LogErrors(<the argument the team must choose>));}
Pick one

Go deeper

Pro tips
  1. If a subclass overrides more than one or two steps, or calls super in the middle of one, the skeleton is wrong: pass functions instead. When: reviewing a hierarchy where every subclass rewrites most of the base. Verify: nothing to run; this is the curriculum row's design advice, and it keys nothing. Status: ADVICE.
  2. PassportStrategy(Strategy, 'staff') registers the strategy under the name you give, so two passport-local logins can live in one app. When: staff and customers log in with different user stores. Verify (RAN, tips.mjs):
tip 2 PassportStrategy(Strategy, 'staff') and (Strategy, 'customer') in one app: staff login 200 {"username":"ana","via":"staff"} · customer login 200 {"username":"bo","via":"customer"} · a staff user at the customer route 401
Source

Every graded behaviour is executed, so no item is keyed on these pages; they back the teaching. Valid as of 2026-10-17.

  • NestJS docs, Passport (authentication) recipe (the row's second source), https://raw.githubusercontent.com/nestjs/docs.nestjs.com/fcc82097df56e6f318960d634c17c2177e17769e/content/recipes/passport.md (nestjs-passport.txt). "With @nestjs/passport, you configure a Passport strategy by extending the PassportStrategy class." and "You provide the verify callback (item 2 above) by implementing a validate() method in your subclass." (steps 5–6); "the guard takes the value returned from the validate() method and assigns it to the Request object as req.user" (step 7); "When we extended the passport-local strategy, @nestjs/passport registered our LocalStrategy with Passport under that default name" (step 9).
  • NestJS docs, Exception filters, section "Inheritance" (not named by the row; teaching only), https://raw.githubusercontent.com/nestjs/docs.nestjs.com/fcc82097df56e6f318960d634c17c2177e17769e/content/exception-filters.md (nestjs-exception-filters.txt). "To delegate exception processing to the base filter, extend BaseExceptionFilter and call the inherited catch() method." (step 8); "The first is to pass the HttpAdapter reference to the constructor when instantiating the custom global filter" (step 9, card 10).
  • The row's first source, Gamma, Helm, Johnson and Vlissides, Design Patterns (1994), Template Method, is a book that cannot be fetched as text, and keys nothing.

Checked against the tools and docs as of 2026-10-17.