Snippets JavaScript / TypeScript
Client fetch minimal
const BASE = 'https://api.wethehivers.com/v1/api';
export async function api<T = unknown>(
path: string,
init: RequestInit & { token?: string } = {},
): Promise<T> {
const headers = new Headers(init.headers);
headers.set('Content-Type', 'application/json');
if (init.token) headers.set('Authorization', `Bearer ${init.token}`);
const r = await fetch(`${BASE}${path}`, { ...init, headers });
if (!r.ok) {
const err = await r.json().catch(() => ({ message: r.statusText }));
throw Object.assign(new Error(err.message), { status: r.status, body: err });
}
return r.status === 204 ? (undefined as T) : r.json();
}
Login + stockage
const { accessToken, refreshToken } = await api<{
accessToken: string;
refreshToken: string;
}>('/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
localStorage.setItem('refresh', refreshToken);
Rechercher des offres
type OffreSearchResult = {
content: Array<{ id: number; titre: string; slug: string }>;
totalElements: number;
totalPages: number;
};
const data = await api<OffreSearchResult>(
'/offres/search?q=java&typeContrat=CDI&page=0&size=20',
);
Upload CV (FormData)
async function uploadCV(file: File, token: string) {
const form = new FormData();
form.append('file', file);
const r = await fetch(`${BASE}/candidats/me/cv`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
body: form,
});
if (!r.ok) throw new Error(`Upload ${r.status}`);
return r.json();
}
Upload avec progression
function uploadCVWithProgress(
file: File,
token: string,
onProgress: (pct: number) => void,
): Promise<{ cvId: number }> {
return new Promise((resolve, reject) => {
const form = new FormData();
form.append('file', file);
const xhr = new XMLHttpRequest();
xhr.open('POST', `${BASE}/candidats/me/cv`);
xhr.setRequestHeader('Authorization', `Bearer ${token}`);
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) onProgress(e.loaded / e.total);
};
xhr.onload = () => (xhr.status < 400 ? resolve(JSON.parse(xhr.response)) : reject(xhr));
xhr.onerror = () => reject(new Error('Network error'));
xhr.send(form);
});
}
Token manager avec auto-refresh
class TokenManager {
private access: string | null = null;
private refresh = localStorage.getItem('refresh');
private pending: Promise<string> | null = null;
async getValidAccess(): Promise<string> {
if (this.access && !this.isExpired(this.access)) return this.access;
if (!this.pending) this.pending = this.doRefresh();
return this.pending;
}
private async doRefresh(): Promise<string> {
if (!this.refresh) throw new Error('No refresh');
const res = await fetch(`${BASE}/auth/refresh-token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken: this.refresh }),
});
if (!res.ok) {
this.logout();
throw new Error('Refresh failed');
}
const { accessToken, refreshToken } = await res.json();
this.access = accessToken;
this.refresh = refreshToken;
localStorage.setItem('refresh', refreshToken);
this.pending = null;
return accessToken;
}
private isExpired(token: string): boolean {
const { exp } = JSON.parse(atob(token.split('.')[1]));
return Date.now() >= (exp - 30) * 1000;
}
logout() {
this.access = null;
this.refresh = null;
localStorage.removeItem('refresh');
}
}
Retry avec backoff (rate limit)
async function fetchWithBackoff(path: string, init: RequestInit, retries = 3) {
for (let i = 0; i <= retries; i++) {
const r = await fetch(`${BASE}${path}`, init);
if (r.status !== 429) return r;
const wait = parseInt(r.headers.get('Retry-After') || '2', 10);
await new Promise((res) => setTimeout(res, (wait + 2 ** i) * 1000));
}
throw new Error('Rate limit persistent');
}
Axios (alternative)
import axios from 'axios';
const api = axios.create({
baseURL: 'https://api.wethehivers.com/v1/api',
});
api.interceptors.request.use(async (config) => {
const token = await tokens.getValidAccess();
config.headers.Authorization = `Bearer ${token}`;
return config;
});
api.interceptors.response.use(
(r) => r,
async (err) => {
if (err.response?.status === 401) {
await tokens.doRefresh();
return api.request(err.config);
}
throw err;
},
);
Types type-safe depuis OpenAPI
npm install -D openapi-typescript
npx openapi-typescript https://api.wethehivers.com/v3/api-docs -o src/api-types.ts
import type { paths } from './api-types';
type LoginBody = paths['/auth/login']['post']['requestBody']['content']['application/json'];
type LoginResponse =
paths['/auth/login']['post']['responses']['200']['content']['application/json'];
const login = async (body: LoginBody): Promise<LoginResponse> =>
api<LoginResponse>('/auth/login', { method: 'POST', body: JSON.stringify(body) });
React Hook : useOffres
import { useQuery } from '@tanstack/react-query';
export function useOffres(query: string, filters: Record<string, string>) {
return useQuery({
queryKey: ['offres', query, filters],
queryFn: () => {
const params = new URLSearchParams({ q: query, ...filters });
return api(`/offres/search?${params}`);
},
staleTime: 30_000,
});
}