Storage va API
Muddatli (TTL) storage va xatolarni o'zbekcha qaytaruvchi yengil API klient.
Ikki kichik, lekin har loyihada kerak bo'ladigan narsa: localStorage ustidagi xavfsiz o'ram va
fetch ustidagi API klient. Ikkalasi ham bog'liqliksiz va SSR'da ishlaydi.
import { createStorage, createApiClient, isApiError } from '@akhatmirzo/core';Storage
Kalit: docs-demo:ism
Natija: —
'use client';
import { createStorage } from '@akhatmirzo/core';
import { Button, Field, Input } from '@akhatmirzo/ui';
import { useState } from 'react';
const store = createStorage({ prefix: 'docs-demo:' });
export default function CoreStorage() {
const [value, setValue] = useState('Sarvar');
const [result, setResult] = useState('—');
const save = (ttl?: number) => {
const ok = store.set('ism', value, { ttl });
setResult(
ok ? `Saqlandi${ttl ? ` (${ttl / 1000} soniya)` : ' (muddatsiz)'}` : "Saqlab bo'lmadi",
);
};
const read = () => {
const saved = store.get<string>('ism');
setResult(saved === null ? "null — yo'q yoki muddati o'tgan" : JSON.stringify(saved));
};
return (
<div className="flex w-full max-w-sm flex-col gap-3">
<Field label="Qiymat" description="Kalit: docs-demo:ism">
<Input value={value} onChange={(e) => setValue(e.target.value)} />
</Field>
<div className="flex flex-wrap gap-2">
<Button variant="primary" onClick={() => save(5000)}>
5 soniyaga saqlash
</Button>
<Button variant="outline" onClick={() => save()}>
Muddatsiz
</Button>
<Button variant="ghost" onClick={read}>
O'qish
</Button>
</div>
<p className="text-sm text-text-muted">
Natija: <code className="font-mono text-text">{result}</code>
</p>
</div>
);
}
createStorage
function createStorage(options?: CreateStorageOptions): TypedStorage;localStorage (yoki boshqa storage) ustidagi o'ram: qiymatlarni JSON qilib saqlaydi, muddat
(ttl) qo'llab-quvvatlaydi va hech qachon xato otmaydi. Storage yo'q (SSR), taqiqlangan
(maxfiy rejim) yoki to'lgan bo'lsa — jim ishlaydi: set false qaytaradi, get — null.
import { createStorage } from '@akhatmirzo/core';
export const storage = createStorage({ prefix: 'app:' });
storage.set('token', 'abc', { ttl: 60 * 60 * 1000 }); // true — 1 soatdan keyin o'chadi
storage.get<string>('token'); // 'abc'
storage.get<string[]>('recent', []); // [] — kalit yo'q bo'lsa fallback
storage.set('user', { id: 7, name: 'Sarvar' }); // obyekt ham saqlanadi
storage.set('user', undefined); // o'chirish
storage.remove('token');
storage.clear(); // faqat "app:" bilan boshlanadigan kalitlar| Prop | Tip | Standart | Tavsif |
|---|---|---|---|
prefix | string | — | Har bir kalit oldiga qo'shiladi, masalan 'app:' |
storage | StorageLike | localStorage | sessionStorage, createMemoryStorage() yoki shu interfeysdagi istalgan obyekt |
TypedStorage metodlari:
| Metod | Nima qiladi |
|---|---|
get<T>(key) | Qiymat yoki null (yo'q, muddati o'tgan yoki buzilgan JSON) |
get<T>(key, fallback) | Qiymat yoki fallback |
set(key, value, { ttl? }) | JSON qilib saqlaydi; ttl — millisekund. undefined — o'chirish. boolean |
remove(key) | Kalitni o'chiradi |
clear() | Prefiksli kalitlarni o'chiradi (prefiks bo'lmasa — hammasini) |
Muddati o'tgan yozuv o'qilganda null qaytadi va storage'dan o'chiriladi.
useLocalStorage bilan bir kalitni ishlatmang
createStorage qiymatni {"v": ..., "e": ...} o'ramida yozadi, useLocalStorage
(hooks) esa oddiy JSON. Ikkalasi bir kalitni o'qisa, bir-birini
tushunmaydi.
createMemoryStorage
function createMemoryStorage(): StorageLike;Xotiradagi storage — testlar uchun yoki localStorage umuman bo'lmagan muhitda.
const storage = createStorage({ storage: createMemoryStorage() });API klient
'use client';
import { createApiClient, isApiError, type FetchLike } from '@akhatmirzo/core';
import { Badge, Button } from '@akhatmirzo/ui';
import { useState } from 'react';
// Haqiqiy server o'rniga: `/status/404` → 404 javob, `/sekin` → hech qachon javob bermaydi
const fakeFetch: FetchLike = (url, init) =>
new Promise((resolve, reject) => {
if (url.endsWith('/sekin')) {
init.signal?.addEventListener('abort', () => reject(new Error('aborted')));
return;
}
const status = Number(url.split('/').pop());
resolve({ ok: status < 400, status, headers: { get: () => null }, text: async () => '' });
});
const api = createApiClient({ baseUrl: '/api', timeout: 1500, fetch: fakeFetch });
export default function CoreApiErrors() {
const [result, setResult] = useState<{ code: string; message: string } | null>(null);
const request = async (path: string) => {
try {
await api.get(path);
setResult({ code: 'OK', message: 'Muvaffaqiyatli' });
} catch (error) {
if (isApiError(error)) setResult({ code: error.code, message: error.message });
}
};
return (
<div className="flex w-full max-w-md flex-col gap-3">
<div className="flex flex-wrap gap-2">
{['401', '403', '404', '422', '500'].map((s) => (
<Button key={s} size="sm" variant="outline" onClick={() => request(`/status/${s}`)}>
{s}
</Button>
))}
<Button size="sm" variant="outline" onClick={() => request('/sekin')}>
Timeout
</Button>
</div>
{result && (
<div className="flex items-center gap-2 text-sm">
<Badge color={result.code === 'OK' ? 'success' : 'danger'}>{result.code}</Badge>
<span className="text-text">{result.message}</span>
</div>
)}
</div>
);
}
Tugmalarni bosing: har bir HTTP status va timeout bitta ApiError turiga, o'zbekcha xabar bilan
aylanadi. Demo haqiqiy server o'rniga soxta fetch ishlatadi.
createApiClient
function createApiClient(options: ApiClientOptions): ApiClient;fetch ustidagi yengil o'ram. U nima qiladi:
baseUrl+ yo'l +querydan URL yasaydi (undefinedvanullqiymatlar tashlanadi);bodyni JSON qiladi vaContent-Type: application/jsonqo'yadi (satr bo'lsa — o'zgarishsiz);getToken()qiymat qaytarsa,Authorization: Bearer <token>qo'shadi;- javobni
Content-Typega qarab JSON yoki matn sifatida qaytaradi; 204, 205 yoki bo'sh javob →undefined; - har qanday muvaffaqiyatsizlikni — HTTP xato, tarmoq, timeout, bekor qilish — bitta
ApiErrorturiga aylantiradi.
import { createApiClient } from '@akhatmirzo/core';
import { storage } from './storage';
export const api = createApiClient({
baseUrl: 'https://api.example.uz/v1',
getToken: () => storage.get<string>('token'),
timeout: 10_000,
});interface Order {
id: number;
total: number;
}
const orders = await api.get<Order[]>('/orders', { query: { page: 2, status: ['new', 'paid'] } });
// GET https://api.example.uz/v1/orders?page=2&status=new&status=paid
const created = await api.post<Order>('/orders', { total: 1250000 });
await api.patch(`/orders/${created.id}`, { total: 1300000 });
await api.delete(`/orders/${created.id}`); // 204 → undefined| Prop | Tip | Standart | Tavsif |
|---|---|---|---|
baseUrlmajburiy | string | — | Masalan 'https://api.example.uz/v1' yoki '/api' |
getToken | () => string | null | undefined | Promise<string | null | undefined> | — | Har so'rovda chaqiriladi; qiymat bo'lsa — Bearer token |
timeout | number | 15000 | Millisekund; 0 — cheklovsiz |
headers | Record<string, string> | — | Har so'rovga qo'shiladigan sarlavhalar |
fetch | FetchLike | globalThis.fetch | O'z fetch'ingiz (testlar, Node, proksi) |
ApiClient metodlari: get(path, options?), delete(path, options?),
post(path, body?, options?), put(path, body?, options?), patch(path, body?, options?) va
umumiy request(method, path, options?). Har bir so'rov uchun parametrlar:
| Prop | Tip | Standart | Tavsif |
|---|---|---|---|
query | Record<string, QueryValue | readonly QueryValue[]> | — | undefined va null tashlanadi; massiv — takroriy kalit |
body | unknown | — | Faqat request() da; post/put/patch'da — 2-argument |
headers | Record<string, string> | — | Shu so'rov sarlavhalari |
signal | AbortSignal | — | So'rovni bekor qilish uchun |
timeout | number | — | Shu so'rov uchun timeout (ms); 0 — cheklovsiz |
Xatolarni ushlash
import { isApiError } from '@akhatmirzo/core';
import { toast } from '@akhatmirzo/ui';
try {
await api.post('/orders', form);
} catch (error) {
if (!isApiError(error)) throw error;
if (error.code === 'HTTP' && error.status === 401) return logout();
toast.error(error.message); // "Serverda xatolik. Keyinroq urinib ko'ring."
}ApiError
class ApiError extends Error {
name: string; // 'ApiError'
readonly status: number; // HTTP status; boshqa xatolarda 0
readonly code: 'HTTP' | 'NETWORK' | 'TIMEOUT' | 'ABORTED' | 'PARSE';
readonly data?: unknown; // server javobi (JSON yoki matn), bo'lsa
}
function isApiError(error: unknown): error is ApiError;code | Qachon | message |
|---|---|---|
HTTP | Server 4xx yoki 5xx qaytardi | Server message maydoni yoki standart matn |
NETWORK | Ulanib bo'lmadi | "Ulanib bo'lmadi. Internetni tekshiring." |
TIMEOUT | timeout dan oshdi | "So'rov vaqti tugadi." |
ABORTED | signal bilan bekor qilindi | "So'rov bekor qilindi." |
PARSE | Javob JSON deb belgilangan, lekin buzuq | "Server javobini o'qib bo'lmadi." |
HTTP xatoda server JSON'ida message satri bo'lsa, o'shani oladi:
{ "message": "Buyurtma topilmadi" } → error.message === 'Buyurtma topilmadi'. Aks holda —
httpErrorMessage(status).
httpErrorMessage
function httpErrorMessage(status: number): string;| Status | Xabar |
|---|---|
| 400 | So'rov noto'g'ri. |
| 401 | Avtorizatsiyadan o'tilmagan. Qaytadan kiring. |
| 403 | Bu amal uchun ruxsat yo'q. |
| 404 | Topilmadi. |
| 409 | Ziddiyat: ma'lumot allaqachon o'zgargan. |
| 422 | Ma'lumotlar noto'g'ri to'ldirilgan. |
| 429 | Juda ko'p so'rov. Birozdan so'ng urinib ko'ring. |
| 5xx | Serverda xatolik. Keyinroq urinib ko'ring. |
| boshqa | So'rov bajarilmadi (418). |
API_MESSAGES — HTTP bo'lmagan xatolar matnlari (NETWORK, TIMEOUT, ABORTED, PARSE)
obyekti.
buildUrl
function buildUrl(baseUrl: string, path: string, query?: QueryParams): string;Klient ichida ishlatiladigan URL yasovchi — alohida ham kerak bo'lishi mumkin (masalan, yuklab
olish havolasi uchun). To'liq URL berilsa, baseUrl e'tiborsiz qoladi.
buildUrl('/api/', '/users', { page: 2, q: undefined }); // '/api/users?page=2'
buildUrl('/api', 'orders', { status: ['new', 'paid'] }); // '/api/orders?status=new&status=paid'
buildUrl('/api', '/search?x=1', { page: 2 }); // '/api/search?x=1&page=2'
buildUrl('/api', 'https://cdn.example.uz/x.json'); // 'https://cdn.example.uz/x.json'