mt-frontend-api-response-handler
v2.0.0
Published
Framework-agnostic TypeScript library for handling standard API responses — success, paginated, validation error, and general error. Includes full DTOs, types, guards, parsers, and helpers.
Maintainers
Readme
api-response-handler
Framework-agnostic TypeScript library for handling standard API responses.
Works with React, Vue, Angular, Svelte, Node.js — or any JS/TS runtime.
Install
npm install api-response-handler
# or
pnpm add api-response-handler
# or
yarn add api-response-handlerResponse Standard
This library parses the following API response shapes:
// ✅ Success
{ success: true, message: string, data: T, errors: null }
// ✅ Paginated
{ success: true, message: string, data: T[], errors: null, pagination: { ... } }
// ❌ Validation Error
{ success: false, message: string, data: null, errors: [{ field, message }] }
// ❌ General Error
{ success: false, message: string, data: null, errors: null }Quick Start
import { handleApiResponse, apiFetch } from 'api-response-handler';
// With the built-in fetch wrapper
const result = await apiFetch<User>('/api/users/1');
switch (result.status) {
case 'success':
console.log(result.data); // User | null
break;
case 'paginated':
console.log(result.data); // User[]
console.log(result.pagination); // PaginationMeta
break;
case 'validation_error':
console.log(result.fieldErrors); // { email: "Email is required" }
break;
case 'general_error':
console.error(result.message);
break;
}
// Or parse a response you already have
const raw = await fetch('/api/data').then(r => r.json());
const parsed = handleApiResponse<User>(raw);API Reference
Core
handleApiResponse<T>(response: unknown): ParsedResponse<T>
Parses any raw API response into a normalized discriminated union result.
apiFetch<T>(url: string, options?: RequestInit): Promise<ParsedResponse<T>>
Drop-in fetch wrapper with built-in parsing. Handles network errors and non-JSON responses gracefully.
Type Guards
isSuccess(response) // → response is RawSuccessResponse<T>
isPaginated(response) // → response is RawPaginatedResponse<T>
isValidationError(response) // → response is RawValidationErrorResponse
isGeneralError(response) // → response is RawGeneralErrorResponseValidation Helpers
import { getFieldError, hasFieldError } from 'api-response-handler';
// result is a ValidationErrorResult
getFieldError(result, 'email') // → "Email is required" | null
hasFieldError(result, 'email') // → true | falsePagination Helpers
import { buildPaginationQuery, getPaginationRange } from 'api-response-handler';
// Build query string
buildPaginationQuery(2, 10) // → "?page=2&limit=10"
fetch(`/api/users${buildPaginationQuery(page, limit)}`);
// Page range for paginator UI (with ellipsis)
getPaginationRange(result.pagination, 2)
// → [1, '...', 4, 5, 6, '...', 10]Constants
import { API_RESPONSE_STATUS } from 'api-response-handler';
API_RESPONSE_STATUS.SUCCESS // "success"
API_RESPONSE_STATUS.PAGINATED // "paginated"
API_RESPONSE_STATUS.VALIDATION_ERROR // "validation_error"
API_RESPONSE_STATUS.GENERAL_ERROR // "general_error"Types
import type {
ParsedResponse,
SuccessResult,
PaginatedResult,
ValidationErrorResult,
GeneralErrorResult,
PaginationMeta,
RawFieldError,
RawApiResponse,
} from 'api-response-handler';Framework Examples
React (custom hook)
import { useState, useCallback } from 'react';
import { apiFetch, getFieldError, type ParsedResponse } from 'api-response-handler';
function useApiCall<T>() {
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<ParsedResponse<T> | null>(null);
const call = useCallback(async (url: string, options?: RequestInit) => {
setLoading(true);
const res = await apiFetch<T>(url, options);
setResult(res);
setLoading(false);
return res;
}, []);
return { call, result, loading };
}
// In a component
function LoginForm() {
const { call, result, loading } = useApiCall<{ token: string }>();
async function handleSubmit(form: { email: string; password: string }) {
await call('/api/auth/login', {
method: 'POST',
body: JSON.stringify(form),
});
}
return (
<>
{result?.status === 'validation_error' && (
<span>{getFieldError(result, 'email')}</span>
)}
{result?.status === 'general_error' && (
<p>{result.message}</p>
)}
</>
);
}Vue 3 (composable)
// composables/useApi.ts
import { ref } from 'vue';
import { apiFetch, type ParsedResponse } from 'api-response-handler';
export function useApi<T>() {
const loading = ref(false);
const result = ref<ParsedResponse<T> | null>(null);
async function call(url: string, options?: RequestInit) {
loading.value = true;
result.value = await apiFetch<T>(url, options);
loading.value = false;
}
return { loading, result, call };
}Svelte 5
<script lang="ts">
import { apiFetch, getFieldError } from 'api-response-handler';
let result = $state<Awaited<ReturnType<typeof apiFetch>> | null>(null);
let loading = $state(false);
async function submit(form: unknown) {
loading = true;
result = await apiFetch('/api/submit', { method: 'POST', body: JSON.stringify(form) });
loading = false;
}
</script>
{#if result?.status === 'validation_error'}
<p>{getFieldError(result, 'email')}</p>
{/if}Angular (service)
@Injectable({ providedIn: 'root' })
export class ApiService {
async get<T>(url: string) {
return apiFetch<T>(url);
}
async post<T>(url: string, body: unknown) {
return apiFetch<T>(url, { method: 'POST', body: JSON.stringify(body) });
}
}License
MIT
