JavaScript · API · Просунутий

Клас API-клієнта

5 завдань

Обгорніть API у багаторазовий клас для чистого та підтримуваного коду на fetch.

Навіщо обгортати API у клас

#
Писати сирі виклики `fetch` скрізь швидко стає повторюванням: кожен запит потребує базового URL, заголовків авторизації та `Content-Type`. Коли API змінюється, доводиться оновлювати безліч місць. Обгортання API у клас вирішує це: ```javascript const client = new APIClient(); await client.login('alice42', 'SecurePass123!'); const products = await client.getProducts({ category: 'chairs' }); await client.addToCart('oak-chair', 2); ``` Клас: - зберігає базовий URL та токен авторизації всередині - будує заголовки один раз і перевикористовує їх - надає зрозумілі назви методів (`login`, `getProducts`) замість сирих HTTP-деталей Ключовий паттерн — допоміжний метод, що автоматично додає заголовки авторизації: ```javascript class APIClient { constructor() { this.baseUrl = 'https://apilearn.tukas.dev'; this.token = null; } async _fetch(path, options = {}) { const headers = { 'Content-Type': 'application/json', ...(this.token ? { 'Authorization': `Token ${this.token}` } : {}), ...options.headers, }; const response = await fetch(`${this.baseUrl}${path}`, { ...options, headers }); return response; } } ``` Кожен публічний метод викликає `this._fetch()`, тому авторизація обробляється в одному місці.

Побудова APIClient крок за кроком

#
Повний APIClient із найкориснішими методами: ```javascript class APIClient { constructor() { this.baseUrl = 'https://apilearn.tukas.dev'; this.token = null; } async _fetch(path, options = {}) { const headers = { 'Content-Type': 'application/json', ...(this.token ? { 'Authorization': `Token ${this.token}` } : {}), ...options.headers, }; const response = await fetch(`${this.baseUrl}${path}`, { ...options, headers }); return response; } async login(username, password) { const data = await this._fetch('/api/auth/token/', { method: 'POST', body: JSON.stringify({ username, password }), }).then(r => r.json()); this.token = data.token; } async getProducts(params = {}) { const qs = new URLSearchParams(params); return this._fetch(`/api/products/?${qs}`).then(r => r.json()) .then(d => d.results); } async getProfile() { return this._fetch('/api/users/profile/').then(r => r.json()); } async addToCart(productSlug, quantity = 1) { return this._fetch('/api/cart/items/', { method: 'POST', body: JSON.stringify({ product_slug: productSlug, quantity }), }).then(r => r.json()); } async clearCart() { await this._fetch('/api/cart/items/', { method: 'DELETE' }); } } // Використання async function main() { const client = new APIClient(); await client.login('YOUR_USERNAME', 'YOUR_PASSWORD'); const products = await client.getProducts({ category: 'chairs' }); console.log(`Знайдено ${products.length} стільців`); const item = await client.addToCart(products[0].slug, 2); console.log('Id позиції кошика:', item.id); const profile = await client.getProfile(); console.log('Увійшли як:', profile.username); await client.clearCart(); } main(); ```

Довідник: сесія та клас клієнта

#
**Скелет класу** ```javascript class APIClient { constructor() { this.baseUrl = '...'; // встановлюється один раз this.token = null; // встановлюється після login } async _fetch(path, options) { /* авторизація + fetch */ } async login(user, pass) { /* встановлює this.token */ } async getProducts(params) { /* викликає _fetch */ } } ``` **Паттерн допоміжного методу _fetch** ```javascript async _fetch(path, options = {}) { const headers = { 'Content-Type': 'application/json', ...(this.token ? { 'Authorization': `Token ${this.token}` } : {}), ...options.headers, // дозволяє перевизначення для окремих викликів }; return fetch(`${this.baseUrl}${path}`, { ...options, headers }); } ``` **Порядок spread-злиття важливий** ```javascript // Пізніші ключі перевизначають ранніші { ...defaults, ...perRequest } // perRequest.Authorization перевизначить defaults.Authorization, якщо присутній ``` **Паттерн для кожного методу** ```javascript // Читання — без тіла async getProfile() { return this._fetch('/api/users/profile/').then(r => r.json()); } // Запис — з тілом async addToCart(slug, quantity = 1) { return this._fetch('/api/cart/items/', { method: 'POST', body: JSON.stringify({ product_slug: slug, quantity }), }).then(r => r.json()); } // Видалення — без тіла відповіді (204) async clearCart() { await this._fetch('/api/cart/items/', { method: 'DELETE' }); } ```
01

Базовий APIClient із login

#

Створіть клас `APIClient` з `constructor`, що встановлює `this.baseUrl` і `this.token = null`, та методом `login(username, password)`, що отримує токен і зберігає його. Перевірте, викликавши `login()` і виведіть `client.token` (перші 20 символів).

class APIClient {
  constructor() {
    // встановити baseUrl і token
  }

  async login(username, password) {
    // отримати токен з /api/auth/token/ і зберегти в this.token
  }
}

async function main() {
  const client = new APIClient();
  await client.login('YOUR_USERNAME', 'YOUR_PASSWORD');
  console.log(client.token.slice(0, 20));
}

main();
Рішення
class APIClient {
  constructor() {
    this.baseUrl = 'https://apilearn.tukas.dev';
    this.token = null;
  }

  async login(username, password) {
    const data = await fetch(`${this.baseUrl}/api/auth/token/`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ username, password }),
    }).then(r => r.json());
    this.token = data.token;
  }
}

async function main() {
  const client = new APIClient();
  await client.login('YOUR_USERNAME', 'YOUR_PASSWORD');
  console.log(client.token.slice(0, 20));
}

main();
02

Додати метод _fetch()

#

Додайте метод `_fetch(path, options = {})`, який автоматично будує заголовки авторизації та викликає `fetch`. Потім додайте `getProfile()`, що використовує `_fetch`. Перевірте обидва.

class APIClient {
  constructor() {
    this.baseUrl = 'https://apilearn.tukas.dev';
    this.token = null;
  }

  async login(username, password) {
    const data = await fetch(`${this.baseUrl}/api/auth/token/`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ username, password }),
    }).then(r => r.json());
    this.token = data.token;
  }

  async _fetch(path, options = {}) {
    // побудувати заголовки з авторизацією, викликати fetch
  }

  async getProfile() {
    // використати _fetch
  }
}

async function main() {
  const client = new APIClient();
  await client.login('YOUR_USERNAME', 'YOUR_PASSWORD');
  const profile = await client.getProfile();
  console.log(profile.username);
}

main();
Рішення
class APIClient {
  constructor() {
    this.baseUrl = 'https://apilearn.tukas.dev';
    this.token = null;
  }

  async login(username, password) {
    const data = await fetch(`${this.baseUrl}/api/auth/token/`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ username, password }),
    }).then(r => r.json());
    this.token = data.token;
  }

  async _fetch(path, options = {}) {
    const headers = {
      'Content-Type': 'application/json',
      ...(this.token ? { 'Authorization': `Token ${this.token}` } : {}),
      ...options.headers,
    };
    return fetch(`${this.baseUrl}${path}`, { ...options, headers });
  }

  async getProfile() {
    return this._fetch('/api/users/profile/').then(r => r.json());
  }
}

async function main() {
  const client = new APIClient();
  await client.login('YOUR_USERNAME', 'YOUR_PASSWORD');
  const profile = await client.getProfile();
  console.log(profile.username);
}

main();
03

Додати getProducts()

#

Додайте метод `getProducts(params = {})`, що приймає об'єкт параметрів і повертає масив `results`. Перевірте без параметрів та з `{ category: "chairs" }`.

class APIClient {
  constructor() {
    this.baseUrl = 'https://apilearn.tukas.dev';
    this.token = null;
  }

  async login(username, password) {
    const data = await fetch(`${this.baseUrl}/api/auth/token/`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ username, password }),
    }).then(r => r.json());
    this.token = data.token;
  }

  async _fetch(path, options = {}) {
    const headers = {
      'Content-Type': 'application/json',
      ...(this.token ? { 'Authorization': `Token ${this.token}` } : {}),
      ...options.headers,
    };
    return fetch(`${this.baseUrl}${path}`, { ...options, headers });
  }

  async getProducts(params = {}) {
    // використати URLSearchParams і _fetch, повернути results
  }
}

async function main() {
  const client = new APIClient();
  await client.login('YOUR_USERNAME', 'YOUR_PASSWORD');
  console.log((await client.getProducts()).length);
  console.log((await client.getProducts({ category: 'chairs' })).length);
}

main();
Рішення
class APIClient {
  constructor() {
    this.baseUrl = 'https://apilearn.tukas.dev';
    this.token = null;
  }

  async login(username, password) {
    const data = await fetch(`${this.baseUrl}/api/auth/token/`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ username, password }),
    }).then(r => r.json());
    this.token = data.token;
  }

  async _fetch(path, options = {}) {
    const headers = {
      'Content-Type': 'application/json',
      ...(this.token ? { 'Authorization': `Token ${this.token}` } : {}),
      ...options.headers,
    };
    return fetch(`${this.baseUrl}${path}`, { ...options, headers });
  }

  async getProducts(params = {}) {
    const qs = new URLSearchParams(params);
    return this._fetch(`/api/products/?${qs}`).then(r => r.json()).then(d => d.results);
  }
}

async function main() {
  const client = new APIClient();
  await client.login('YOUR_USERNAME', 'YOUR_PASSWORD');
  console.log((await client.getProducts()).length);
  console.log((await client.getProducts({ category: 'chairs' })).length);
}

main();
04

Повний APIClient

#

Побудуйте повний APIClient із методами: `login()`, `_fetch()`, `getProducts(params)`, `getProfile()`, `addToCart(slug, qty)`, `clearCart()`. Потім послідовно використайте всі методи.

class APIClient {
  // реалізувати всі методи
}

async function main() {
  const client = new APIClient();
  await client.login('YOUR_USERNAME', 'YOUR_PASSWORD');

  const products = await client.getProducts();
  console.log('Товарів:', products.length);

  const item = await client.addToCart(products[0].slug);
  console.log('Id позиції кошика:', item.id);

  const profile = await client.getProfile();
  console.log('Користувач:', profile.username);

  await client.clearCart();
  console.log('Кошик очищено');
}

main();
Рішення
class APIClient {
  constructor() {
    this.baseUrl = 'https://apilearn.tukas.dev';
    this.token = null;
  }

  async _fetch(path, options = {}) {
    const headers = {
      'Content-Type': 'application/json',
      ...(this.token ? { 'Authorization': `Token ${this.token}` } : {}),
      ...options.headers,
    };
    return fetch(`${this.baseUrl}${path}`, { ...options, headers });
  }

  async login(username, password) {
    const data = await this._fetch('/api/auth/token/', {
      method: 'POST',
      body: JSON.stringify({ username, password }),
    }).then(r => r.json());
    this.token = data.token;
  }

  async getProducts(params = {}) {
    const qs = new URLSearchParams(params);
    return this._fetch(`/api/products/?${qs}`).then(r => r.json()).then(d => d.results);
  }

  async getProfile() {
    return this._fetch('/api/users/profile/').then(r => r.json());
  }

  async addToCart(productSlug, quantity = 1) {
    return this._fetch('/api/cart/items/', {
      method: 'POST',
      body: JSON.stringify({ product_slug: productSlug, quantity }),
    }).then(r => r.json());
  }

  async clearCart() {
    await this._fetch('/api/cart/items/', { method: 'DELETE' });
  }
}

async function main() {
  const client = new APIClient();
  await client.login('YOUR_USERNAME', 'YOUR_PASSWORD');

  const products = await client.getProducts();
  console.log('Products:', products.length);

  const item = await client.addToCart(products[0].slug);
  console.log('Cart item id:', item.id);

  const profile = await client.getProfile();
  console.log('User:', profile.username);

  await client.clearCart();
  console.log('Cart cleared');
}

main();
05

Додати метод placeOrder()

#

Розширте APIClient методом `placeOrder(phoneNumber)`. Потім використайте клієнт для: входу, додавання товару, оформлення замовлення та виведення `id` і `status` замовлення.

class APIClient {
  constructor() {
    this.baseUrl = 'https://apilearn.tukas.dev';
    this.token = null;
  }

  async _fetch(path, options = {}) {
    const headers = {
      'Content-Type': 'application/json',
      ...(this.token ? { 'Authorization': `Token ${this.token}` } : {}),
      ...options.headers,
    };
    return fetch(`${this.baseUrl}${path}`, { ...options, headers });
  }

  async login(username, password) {
    const data = await this._fetch('/api/auth/token/', {
      method: 'POST',
      body: JSON.stringify({ username, password }),
    }).then(r => r.json());
    this.token = data.token;
  }

  async getProducts(params = {}) {
    const qs = new URLSearchParams(params);
    return this._fetch(`/api/products/?${qs}`).then(r => r.json()).then(d => d.results);
  }

  async addToCart(productSlug, quantity = 1) {
    return this._fetch('/api/cart/items/', {
      method: 'POST',
      body: JSON.stringify({ product_slug: productSlug, quantity }),
    }).then(r => r.json());
  }

  async placeOrder(phoneNumber) {
    // POST /api/orders/ і повернути результат
  }
}

async function main() {
  const client = new APIClient();
  await client.login('YOUR_USERNAME', 'YOUR_PASSWORD');
  const products = await client.getProducts();
  await client.addToCart(products[0].slug);
  const order = await client.placeOrder('+1234567890');
  console.log(order.id);
  console.log(order.status);
}

main();
Рішення
class APIClient {
  constructor() {
    this.baseUrl = 'https://apilearn.tukas.dev';
    this.token = null;
  }

  async _fetch(path, options = {}) {
    const headers = {
      'Content-Type': 'application/json',
      ...(this.token ? { 'Authorization': `Token ${this.token}` } : {}),
      ...options.headers,
    };
    return fetch(`${this.baseUrl}${path}`, { ...options, headers });
  }

  async login(username, password) {
    const data = await this._fetch('/api/auth/token/', {
      method: 'POST',
      body: JSON.stringify({ username, password }),
    }).then(r => r.json());
    this.token = data.token;
  }

  async getProducts(params = {}) {
    const qs = new URLSearchParams(params);
    return this._fetch(`/api/products/?${qs}`).then(r => r.json()).then(d => d.results);
  }

  async addToCart(productSlug, quantity = 1) {
    return this._fetch('/api/cart/items/', {
      method: 'POST',
      body: JSON.stringify({ product_slug: productSlug, quantity }),
    }).then(r => r.json());
  }

  async placeOrder(phoneNumber) {
    return this._fetch('/api/orders/', {
      method: 'POST',
      body: JSON.stringify({ phone_number: phoneNumber }),
    }).then(r => r.json());
  }
}

async function main() {
  const client = new APIClient();
  await client.login('YOUR_USERNAME', 'YOUR_PASSWORD');
  const products = await client.getProducts();
  await client.addToCart(products[0].slug);
  const order = await client.placeOrder('+1234567890');
  console.log(order.id);
  console.log(order.status);
}

main();