Understanding Shallow vs Deep Copying in JavaScript: A Next.js API Example
One common pitfall I encounter is the difference between shallow and deep copying of objects, especially when dealing with nested data structures. This blog post will explore this issue through a real-world example involving an API response in a Next.js project.
Let's imagine that you have a default API response object defined as follows:
const defaultApiResponse: ApiResponse<any> = {
data: [],
success: false,
message: "No data found",
errors: [],
status: 404
};
Now, you create an API endpoint that uses this default response as a template for building actual responses:
import { NextRequest, NextResponse } from 'next/server';
import prisma from '../../../../database/prismaClient';
import { defaultApiResponse } from '../ApiResponseType';
export async function GET(req: NextRequest) {
const apiResponse = { ...defaultApiResponse };
try {
const { searchParams } = new URL(req.url);
let product_id = searchParams.get('product_id');
if (!product_id || isNaN(Number(product_id))) {
apiResponse.errors.push("Product ID is required and must be a number");
apiResponse.success = false;
apiResponse.status = 400;
return NextResponse.json(apiResponse);
}
const product = await prisma.product.findUnique({
where: { id: Number(product_id) }
});
if (!product) {
apiResponse.errors.push("Product not found");
apiResponse.success = false;
apiResponse.status = 404;
} else {
apiResponse.data.push(product);
apiResponse.success = true;
apiResponse.status = 200;
apiResponse.message = "Product found";
}
} catch (error) {
apiResponse.success = false;
apiResponse.status = 500;
apiResponse.errors.push(error.message || "An unknown error occurred");
}
return NextResponse.json(apiResponse);
}
Initially, this code seems correct. However, upon making several requests, you notice that the apiResponse
object retains data from previous requests.
Why is this happening? 🤔
Due to Shallow Copy
But wait, what is Shallow copy? 🤔
Understanding Shallow Copy
The line:
const apiResponse = { ...defaultApiResponse };
uses the spread operator to create a new object. This is a shallow copy, meaning it only copies the top-level properties.
If any of these properties are objects or arrays (like data
and errors
), the new object will reference the same memory locations as the original.
Thus, modifying apiResponse.errors
or apiResponse.data
will also modify defaultApiResponse.errors
and defaultApiResponse.data
, leading to a persistent state across requests.
The Solution: Deep Copy
To prevent this issue, you need to ensure that each request gets a completely independent copy of the defaultApiResponse
, including its nested arrays and objects.
One way to achieve this is by using a utility function to create a fresh copy:
Step 1: Create a utility function
// utils/apiResponse.ts
import { ApiResponse } from '../ApiResponseType';
export function getDefaultApiResponse<T>(): ApiResponse<T> {
return {
data: [],
success: false,
message: "No data found",
errors: [],
status: 404
};
}
Step 2: Use the utility function in your API handler
import { NextRequest, NextResponse } from 'next/server';
import prisma from '../../../../database/prismaClient';
import { getDefaultApiResponse } from '../../../../utils/apiResponse';
export async function GET(req: NextRequest) {
const apiResponse = getDefaultApiResponse<any>();
try {
const { searchParams } = new URL(req.url);
let product_id = searchParams.get('product_id');
if (!product_id || isNaN(Number(product_id))) {
apiResponse.errors.push("Product ID is required and must be a number");
apiResponse.success = false;
apiResponse.status = 400;
return NextResponse.json(apiResponse);
}
const product = await prisma.product.findUnique({
where: { id: Number(product_id) }
});
if (!product) {
apiResponse.errors.push("Product not found");
apiResponse.success = false;
apiResponse.status = 404;
} else {
apiResponse.data.push(product);
apiResponse.success = true;
apiResponse.status = 200;
apiResponse.message = "Product found";
}
} catch (error) {
apiResponse.success = false;
apiResponse.status = 500;
apiResponse.errors.push(error.message || "An unknown error occurred");
}
return NextResponse.json(apiResponse);
}
Now you need to think about Why This Works. 🤔
So to answer this:
The utility function getDefaultApiResponse
ensures that every request handler invocation starts with a fresh, deep copy of the default response. This prevents shared references and ensures that modifications to apiResponse
do not affect defaultApiResponse
or other instances of apiResponse
.
Conclusion
Understanding the difference between shallow and deep copying in JavaScript is crucial when dealing with objects and arrays. Shallow copies can lead to unintended side effects, especially in stateful applications like APIs.
By using utility functions to create fresh copies of default objects, you can avoid these pitfalls and ensure your code remains clean and maintainable.
Thanks for reading my post. If you have any questions regarding this, let me know.
Top comments (0)