Caching in Software Engineering: Optimizing Performance with NestJS and Next.js RTK

Milad Fahmy
6 min readOct 2, 2024

--

Generated by AI

In today’s fast-paced digital landscape, optimizing application performance is crucial for delivering a seamless user experience. One of the most effective strategies to enhance performance is caching. By temporarily storing frequently accessed data, caching can significantly reduce latency, lower server load, and improve the scalability of your software.

In this article, we will explore caching in three parts: an overview of caching in software engineering, backend caching implementation using NestJS, and frontend caching with Next.js using Redux Toolkit Query (RTK).

Caching in General in Software Engineering

What is Caching?

Caching is the process of storing data temporarily in a storage layer that is quicker to access than the original source. This means that when a request for data is made, the system can first check if the data is available in the cache before querying the original source, such as a database or an external API.

Why Caching is Important

Caching is essential in software engineering for several reasons:

1. Improved Performance: By retrieving data from the cache rather than the original source, applications can respond to requests faster, significantly enhancing user experience.

2. Reduced Load on Backend Services: Caching decreases the number of requests made to databases or APIs, reducing the load on these resources and allowing them to serve other requests more efficiently.

3. Cost Efficiency: In cloud-based environments, minimizing the number of server requests can lower operational costs by reducing compute time and bandwidth usage.

4. Scalability: Caching strategies allow applications to handle more simultaneous users and requests without degrading performance.

Types of Caching

1. In-memory Caching: Data is stored in RAM, providing ultra-fast access. Commonly used for frequently accessed data.

Examples: Redis, Memcached.

2. Database Caching: Caches query results or frequently accessed data directly in the database layer.

Examples: MySQL query cache, PostgreSQL caching.

3. API Response Caching: Stores responses from external API calls to minimize redundant requests.

Examples: Caching API responses in Redis.

4. Browser Caching: Stores static assets like JavaScript, CSS, and images locally in the user’s browser.

5. Content Delivery Network (CDN) Caching: Caches static files on servers geographically closer to users, reducing latency.

Examples: Cloudflare, AWS CloudFront.

Best Practices for Caching

Choose the Right Caching Strategy: Select an appropriate caching strategy based on data volatility, read/write frequency, and required latency.

Set Appropriate TTL (Time to Live): Configure cache expiration to balance freshness and performance.

Monitor Cache Performance: Regularly assess cache hit rates and evictions to optimize caching strategies.

  • Implement Cache Invalidation: Ensure that stale data is removed or updated appropriately when underlying data changes.

Backend Caching with NestJS

NestJS is a progressive Node.js framework used for building scalable server-side applications. Its built-in support for caching makes it easy to store frequently accessed data, thus enhancing application performance.

Benefits of Caching in NestJS

1. Improved Performance: Caching helps serve data quickly, particularly for expensive database queries.

2. Reduced Load on Database: It minimizes the frequency of database access, thereby lowering costs and improving response times.

3. Simplified Development: NestJS provides decorators and modules that streamline the implementation of caching strategies.

Implementing Caching in NestJS

Step 1: Install Dependencies

To get started, install the necessary packages:

npm install --save @nestjs/cache-manager cache-manager

Step 2: Set Up the Cache Module

In your AppModule, import and configure the CacheModule. You can specify options like ttl (Time to Live) and max (maximum number of cache entries).

import { Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
imports: [
CacheModule.register({
ttl: 60, // Cache items for 60 seconds
max: 100, // Maximum of 100 items in the cache
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

Step 3: Use Caching in a Service

Inject CACHE_MANAGER into your service to implement caching logic. For instance, let’s cache the result of an expensive database operation.

import { Injectable, CacheStore, Inject } from '@nestjs/common';
import { Cache } from 'cache-manager';

@Injectable()
export class AppService {
constructor(@Inject(CACHE_MANAGER) private cacheManager: CacheStore) {}

async getCachedData(key: string): Promise<any> {
const cachedData = await this.cacheManager.get(key);
if (cachedData) {
return cachedData; // Return cached data if available
}

const freshData = await this.expensiveDatabaseOperation();
await this.cacheManager.set(key, freshData, { ttl: 60 }); // Cache fresh data for 60 seconds
return freshData;
}

private async expensiveDatabaseOperation(): Promise<any> {
// Simulate an expensive operation
return new Promise((resolve) => {
setTimeout(() => resolve({ message: 'Data fetched from database' }), 2000);
});
}
}

Step 4: Implement Caching in the Controller

Use your caching service in a controller to serve cached data.

import { Controller, Get, Query } from '@nestjs/common';
import { AppService } from './app.service';

@Controller('data')
export class AppController {
constructor(private readonly appService: AppService) {}

@Get()
async getData(@Query('key') key: string) {
return this.appService.getCachedData(key);
}
}

Conclusion for Backend Caching

By implementing caching in NestJS, you can significantly enhance your application’s performance and scalability. The @nestjs/cache-manager module makes it straightforward to set up caching for your data-fetching operations.

Frontend Caching with Next.js and RTK

On the frontend, efficient data fetching and caching are crucial for creating responsive applications. Next.js, a React framework, combined with Redux Toolkit Query (RTK), offers a powerful solution for managing API data with built-in caching capabilities.

Benefits of Caching in Next.js with RTK

1. Optimized Performance: Caching reduces the need for redundant network requests, improving response times.

2. Simplified Data Management: RTK provides a streamlined approach to managing data fetching and caching in your React components.

3. Automatic Cache Invalidation: RTK automatically invalidates stale data, ensuring users always receive the most recent information.

Implementing Caching in Next.js with RTK

Step 1: Install Dependencies

First, install Redux Toolkit and the required libraries:

npm install @reduxjs/toolkit react-redux

Step 2: Set Up the API Slice

Create an API slice using RTK Query, where you define your endpoints and caching behavior.

// apiSlice.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (builder) => ({
getPosts: builder.query({
query: () => 'posts',
keepUnusedDataFor: 60, // Keep data in cache for 60 seconds
}),
addPost: builder.mutation({
query: (newPost) => ({
url: 'posts',
method: 'POST',
body: newPost,
}),
invalidatesTags: ['Post'], // Invalidate 'Post' tags upon mutation
}),
}),
});

export const { useGetPostsQuery, useAddPostMutation } = api;

Step 3: Provide the Store

Wrap your application with the Redux Provider and integrate the API slice into your Redux store.

// _app.tsx
import { Provider } from 'react-redux';
import { store } from '../store'; // Assuming store is set up
import { api } from '../apiSlice'; // Import your API slice

const MyApp = ({ Component, pageProps }) => (
<Provider store={store}>
<Component {...pageProps} />
</Provider>
);

export default MyApp;

Step 4: Use the Query in a Component

Use the useGetPostsQuery hook in your component to fetch and display data.

// PostsList.tsx
import React from 'react';
import { useGetPostsQuery } from '../apiSlice';

const PostsList = () => {
const { data: posts, error, isLoading } = useGetPostsQuery();

if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error occurred: {error.message}</div>;

return (
<div>
{posts.map((post) => (
<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</div>
))}
</div>
);
};

export default PostsList;

Conclusion for Frontend Caching

Using Next.js with Redux Toolkit Query (RTK) provides a robust solution for managing data fetching and caching on the frontend. By leveraging built-in caching strategies, you can optimize performance, reduce unnecessary network requests, and enhance user experience.

Final Thoughts

Caching is a fundamental concept in software engineering that significantly enhances application performance and scalability. By implementing effective caching strategies in both backend systems like NestJS and frontend frameworks like Next.js with RTK, developers can create efficient, responsive applications that meet the demands of modern users. Understanding and applying these caching principles is essential for any software engineer looking to improve their application’s efficiency and user experience.

--

--

Milad Fahmy
Milad Fahmy

Written by Milad Fahmy

I’m a JS Champion, I wrote about software engineering. I’m currently developing various open-source projects (like: https://encrypt-rsa.js.org)

No responses yet