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();