We’ve all been there: you start a new project, and before you’ve written a single line of business logic, your package.json is already filled with small dependancies, and complex monorepo configurations.
While libraries are great, they often come with extra weight and "abstraction tax" that you don't always need.
In this post, I’m sharing couple of lightweight patterns I use to slash boilerplate and remove third-party dependencies in my Next.js and NestJS applications.
1. Fetch Wrapper
If you fetch apis using native apis, you can reduce a lot of code by using this wrapper
// lib/api-client.ts
import Cookies from "js-cookie";
const baseUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000";
export async function clientFetch<T>(
endpoint: string,
options: RequestInit = {},
): Promise<T> {
const token = Cookies.get("token");
const headers: Record<string, string> = {
"Content-Type": "application/json",
...(token && { Authorization: `Bearer ${token}` }),
...(options.headers as Record<string, string>),
};
if (options.body instanceof FormData) {
delete headers["Content-Type"];
}
const response = await fetch(`${baseUrl}${endpoint}`, {
...options,
headers,
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message);
}
return response.json();
}
Use examples
// favorites.service.ts
export const favoritesService = {
getAll: () => clientFetch<IProduct[]>("/favorites"),
createOne: (productId: string) =>
clientFetch<IProduct>(`/favorites/${productId}`, { method: "POST" }),
removeOne: (productId: string) =>
clientFetch<null>(`/favorites/${productId}`, { method: "DELETE" }),
};
2. JavaScript Object to formdata
Do you find yourself appending object keys and values manually to convert javascript objects to formdata? Which result in ugly and unmaintainable code? Use this instead
// utils/helper.ts
export function jsonToFormData(data: any) {
const formData = new FormData();
buildFormData(formData, data);
return formData;
}
function buildFormData(formData: any, data: any, parentKey?: any) {
if (
data &&
typeof data === "object" &&
!(data instanceof Date) &&
!(data instanceof File) &&
!(data instanceof Blob)
) {
Object.keys(data).forEach((key) => {
buildFormData(
formData,
data[key],
parentKey ? `${parentKey}[${key}]` : key,
);
});
} else {
const value = data == null ? "" : data;
formData.append(parentKey, value);
}
}
Use examples
// products.service.ts
export const productsService = {
create: (product: ICreateProduct) => {
const formData = jsonToFormData(product);
return clientFetch<IProduct>("/products", {
method: "POST",
body: formData,
});
},
updateOne: (id: string, product: IUpdateProduct) => {
const formData = jsonToFormData(product);
return clientFetch<IProduct>(`/products/${id}`, {
method: "PATCH",
body: formData,
});
},
};
3. Duplicate Interfaces
Do you see yourself duplicating interfaces between front-end and back-end? Well you may know about monorepo tools like Turborepo, Nx. But you don’t want to introduce that much complexity to just share the typescript files. Do this.
I assume you are using Next.js and NestJS directory as following
/api/
/web/
/shared/
/src/
user.type.ts
product.type.ts
Add this tsconfig.ts to your Next.js and NestJS
// web/tsconfig.json
{
"compilerOptions": {
"paths": {
"@shared/types/*": ["../shared/src/*"]
}
},
"include": [
"../shared/src/**/*"
],
}
Then you can import them
import { User, CreateUser, UpdateUser } from "@shared/user.type";
import { Product, CreateProduct, UpdateProduct } from "@shared/product.type";
4. Class Merging
If you are tired of long string concatenations for CSS classes but don't want a library
// lib/utils.ts
export function cn(...classes: (string | boolean | undefined)[]) {
return classes.filter(Boolean).join(" ");
}
Use examples
// Before
<button className={`btn ${active ? 'btn-active' : ''} ${disabled ? 'btn-disabled' : ''}`}>
Submit
</button>
// After
<button className={cn("btn", active && "btn-active", disabled && "btn-disabled")}>
Submit
</button>
Wrapping Up
By leaning on the native Fetch API, recursive FormData builders, and TypeScript’s path mapping, you can keep your architecture lean and your bundle size small. You don’t always need a 50kb library to handle a task that a 20-line utility function can do better.
What about you? Are there any third-party libraries you’ve successfully replaced with native code recently? I’d love to hear your favorite "vanilla" hacks in the comments below!
Top comments (0)