Clean API Layer in Node.js with mvc-front-sdk
Most Node.js projects end up with API calls scattered across files — fetch calls living in components, service files with no clear structure, and token management duplicated everywhere. mvc-front-sdk is a small library I built to fix that. It gives you a structured, type-safe controller pattern that works in any JavaScript environment, including plain Node.js.

Installation
npm install mvc-front-sdk # or pnpm add mvc-front-sdk
Requirements: Node.js >= 18, TypeScript ^5.
The problem it solves
Without structure, API code tends to look like this — spread across your project with no clear ownership:
// Scattered everywhere const res = await fetch(`${BASE_URL}/users/${id}`, { headers: { Authorization: `Bearer ${token}` }, }); const user = await res.json();
With mvc-front-sdk, you define a controller once and use it everywhere cleanly.
Setting up your first controller
Extend BaseController with your base URL and optional auth token. The constructor handles token injection into every request automatically.
// src/controllers/user.controller.ts import { BaseController } from "mvc-front-sdk"; type User = { id: string; name: string; email: string; }; type CreateUserDto = { name: string; email: string; }; export class UserController extends BaseController { constructor() { super( process.env.API_BASE_URL!, process.env.API_TOKEN, // Optional — adds Authorization: Bearer <token> to all requests ); } async getAll(filters?: { page?: number; limit?: number }) { const params = this.buildSearchParams(filters); const url = this.createURL("/users", params); return this.apiService.get<User[]>(url); } async getById(id: string) { return this.apiService.get<User>(`/users/${id}`); } async create(data: CreateUserDto) { return this.apiService.post<User>("/users", data); } async update(id: string, data: Partial<User>) { return this.apiService.put<User>(`/users/${id}`, data); } async remove(id: string) { return this.apiService.delete(`/users/${id}`); } }
Using it in a script or service
Instantiate the controller and call its methods. Everything is typed — no any, no manual header wiring.
// src/scripts/sync-users.ts import { UserController } from "../controllers/user.controller"; const users = new UserController(); async function run() { const list = await users.getAll({ page: 1, limit: 50 }); console.log(`Fetched ${list.length} users`); const newUser = await users.create({ name: "John Doe", email: "john.doe@example.com", }); console.log("Created:", newUser.id); } run();
Query parameters with buildSearchParams
The buildSearchParams helper converts a plain object into URLSearchParams, with support for renaming keys and custom transforms:
const params = this.buildSearchParams( { name: "Fariol", page: 1, tags: ["ts", "node"] }, { rename: { name: "fullName" }, // rename key before sending transform: { page: (v) => String(v) }, }, ); // → fullName=Fariol&page=1&tags=ts,node
Then compose the URL:
const url = this.createURL("/users", params); // → /users?fullName=Fariol&page=1&tags=ts,node
Default headers
If your API requires versioning headers or a client ID on every request, pass them once in the constructor:
super("https://api.example.com", process.env.API_TOKEN, { "X-API-Version": "v1", "X-Client-ID": "my-node-service", });
Headers passed directly to apiService.get(...) will override defaults for that specific call:
await this.apiService.get("/users", { "X-API-Version": "v2", // overrides v1 for this request only });
Error handling with ApiError
The SDK throws typed ApiError instances, so you can handle HTTP errors explicitly:
import { ApiError } from "mvc-front-sdk"; try { await users.getById("nonexistent-id"); } catch (error) { if (error instanceof ApiError) { console.error(error.statusCode); // e.g. 404 console.error(error.message); // error message console.error(error.isUnAuthenticated()); // true if 401 } }
Or delegate to the built-in handler inside your controller:
async getById(id: string) { try { return await this.apiService.get<User>(`/users/${id}`); } catch (error) { this.handleError(error); // re-throws with formatted message } }
Dynamic token refresh
If your token expires during a session (e.g. after a login/refresh flow), update it at runtime without recreating the controller:
export class AuthController extends BaseController { constructor() { super("https://api.example.com"); } async login(email: string, password: string) { const { token } = await this.apiService.post<{ token: string }>( "/auth/login", { email, password }, ); this.apiService.setToken(token); return token; } logout() { this.apiService.setToken(undefined); } }
FormData support
File uploads work out of the box — pass a FormData instance as the body:
async uploadAvatar(userId: string, file: File) { const form = new FormData(); form.append("file", file); form.append("userId", userId); return this.apiService.post<{ url: string }>("/upload", form); }
Project structure recommendation
src/ ├── controllers/ │ ├── user.controller.ts │ ├── auth.controller.ts │ └── product.controller.ts ├── scripts/ │ └── sync-users.ts └── index.ts
Keep one controller per resource. Each controller owns all the HTTP logic for that resource — nothing leaks out.
Source
The library is open source. You can find it on GitHub and install it from npm as mvc-front-sdk.