This module showed several tools: a cache in the process's memory and in Redis, database indexes, response compression, limiting the number of requests. The project is where you must decide which one to use - and that is a quite different skill from knowing the syntax.
You will build a legion management API that copes with more traffic than a naive version would bear, and can show why it copes.
Optimisation without measurement is guesswork. It sounds obvious, and it is the most frequently broken rule in the field: a cache is added wherever it happened to come to mind, and an improvement nobody measured is duly recorded.
The order of work is always the same:
That is why the project begins with measurement, not with a cache.
The first thing you expose is a view of the process's state:
1@Controller('metrics')
2export class MetricsController {
3 @Get()
4 getMetrics() {
5 const memory = process.memoryUsage();
6
7 return {
8 heapUsed: memory.heapUsed,
9 heapTotal: memory.heapTotal,
10 rss: memory.rss,
11 uptime: process.uptime(),
12 };
13 }
14}process.memoryUsage() returns several numbers, and it is worth knowing how they differ. heapUsed is the memory actually occupied by your application's objects - it grows when you keep too much in a cache. heapTotal is the memory the V8 engine has reserved for the heap; it is always larger and changes in jumps. rss (Resident Set Size) is the whole process's memory as the operating system sees it - heap, stack, code and buffers together.The difference between
heapUsed and rss matters most. When heapUsed alone grows, you are accumulating objects - usually in some cache without a limit. When rss grows while heapUsed stands still, the leak is outside the heap: buffers, open connections, native libraries.
gives the number of seconds the process has been running. It says little by itself, but a great deal alongside the rest: memory use rising linearly with running time is a leak, not load.process.uptime()
The in-process cache you met earlier has one flaw: every instance of the application has its own. With two instances the same read reaches the database twice, and an invalidation in one never reaches the other. Redis fixes that:
1CacheModule.register({
2 store: redisStore,
3 host: 'localhost',
4 port: 6379,
5 ttl: 300 })The written order is fixed:
opens the configuration, CacheModule.register({
swaps the store from process memory to Redis, store: redisStore,
and host: 'localhost',
say where that Redis stands, and port: 6379,
sets the default lifetime of an entry and closes.ttl: 300 })
store is the crucial line. Without it CacheModule still works, but in process memory - and that is treacherous, because locally everything looks fine and the problem appears only once a second instance starts in production.Note that
host and port sit directly in the configuration object here. Newer versions of cache-manager nest them in a separate connection field - check the version in your package.json before copying somebody else's example.A cache speeds up repeated reads. Queries that cannot be cached must be fixed at the source:
1@Entity('legionaries')
2@Index(['cohortId', 'isActive'])
3export class Legionary {
4 @Column()
5 cohortId: number;
6
7 @Column({ default: true })
8 isActive: boolean;
9}
10
11// in the repository
12findActiveByCohort(cohortId: number) {
13 return this.repo.find({
14 where: { cohortId, isActive: true },
15 select: ['id', 'name', 'rank'],
16 take: 50,
17 });
18}Three fixes at once, each acting differently. A composite index on the columns you genuinely filter by turns a scan of the whole table into a jump to particular rows.
fetches only the needed columns - without it you drag fields nobody will use out of the database, bulk and all. select
limits the number of rows; an endpoint without a limit works splendidly until the day somebody has a hundred thousand of them.take
Before adding an index, check the query plan with
EXPLAIN. An index on a column nobody filters by speeds up no read and slows down every write.Two things at the application's edge remain:
1async function bootstrap() {
2 const app = await NestFactory.create(AppModule);
3
4 app.use(compression());
5
6 await app.listen(3000);
7}compression() packs responses with gzip - for JSON lists that usually means several times less traffic, at the cost of a little processor time. Rate limiting with ThrottlerModule works from the other side: it speeds nothing up, it limits the number of requests you accept at all.These two are worth understanding as a pair of opposites. Compression and caching make one request cost less. Rate limiting makes the requests fewer. When a system is choking you always have those two roads open - and usually you need both.
The project is finished when it contains:
/metrics endpoint with heapUsed, heapTotal, rss and uptime.CacheModule.register with store: redisStore, invalidated on write.select and take in listing queries.The fifth point matters most and is most often skipped. Without it you are handing in a collection of techniques rather than an optimisation - because an optimisation is the difference between two measurements, @name, not a list of tools applied.
Send the link to your repository when you are done.