Jak napsat API Wrapper pomocí tříd JavaScriptu a načítání

Jak napsat obálku rozhraní API pomocí tříd JavaScriptu, které volají rozhraní API JSON Placeholder pomocí pohodlných a snadno zapamatovatelných metod prostřednictvím funkce Fetch.

Začínáme

Pro tento tutoriál použijeme full-stack JavaScriptový framework CheatCode, Joystick. Joystick spojuje rozhraní front-end UI s back-endem Node.js pro vytváření aplikací.

Pro začátek budeme chtít nainstalovat Joystick přes NPM. Před instalací se ujistěte, že používáte Node.js 16+, abyste zajistili kompatibilitu (pokud se potřebujete naučit, jak nainstalovat Node.js nebo spustit více verzí na vašem počítači, přečtěte si nejprve tento tutoriál):

Terminál

npm i -g @joystick.js/cli

Tím se Joystick nainstaluje globálně do vašeho počítače. Po instalaci vytvořte nový projekt:

Terminál

joystick create app

Po několika sekundách se zobrazí zpráva o odhlášení na cd do nového projektu a spusťte joystick start :

Terminál

cd app && joystick start

Poté by vaše aplikace měla být spuštěna a my jsme připraveni začít.

Psaní třídy obalu API

Pro tento tutoriál napíšeme obal pro JSON Placeholder API, bezplatné HTTP REST API pro testování. Naším cílem je vytvořit znovu použitelný „obal“, který nám pomůže zefektivnit proces odesílání požadavků do API.

Nejprve vytvoříme samotný obal API jako třídu JavaScriptu. To nám poskytne způsob, jak – pokud si to přejeme – vytvořit více instancí našeho obalu. Uvnitř aplikace, kterou jsme právě vytvořili, otevřeme /api složku v kořenovém adresáři projektu a vytvořte nový soubor na /api/jsonplaceholder/index.js :

/api/jsonplaceholder/index.js

class JSONPlaceholder {
  constructor() {
    this.endpoints = {};
  }
}

export default new JSONPlaceholder();

Při vytváření kostry pro náš obal jsme zde nastavili základní třídu JavaScriptu s constructor() funkce — co se volá bezprostředně po new klíčové slovo je voláno ve třídě JavaScriptu – která nastavuje prázdný objekt na třídě this.endpoints . Uvnitř, jak budeme postupovat, vybudujeme tento this.endpoints objekt, který obsahuje metody (funkce definované na objektu) pro dynamické generování požadavků HTTP, které má náš wrapper provádět.

Ve spodní části našeho souboru, i když technicky můžeme exportovat pouze samotnou třídu (bez new klíčové slovo), zde pro testování vytvoříme jednu instanci a exportujeme ji jako export default new JSONPlaceholder() . To nám umožní importovat a volat do našeho obálky přímo odjinud v naší aplikaci, aniž bychom museli nejprve udělat něco takového:

import JSONPlaceholder from 'api/jsonplaceholder/index.js';

const jsonPlaceholder = new JSONPlaceholder();

jsonPlaceholder.posts('list');

Místo toho budeme moci udělat:

import jsonPlaceholder from './api/jsonplaceholder/index.js';

jsonPlaceholder.posts('list');

Abychom viděli, jak se k tomuto bodu dostaneme, pojďme dále vytvořit this.endpoints objekt v konstruktoru a vysvětlete, jak nám to pomůže provádět požadavky.

/api/jsonplaceholder/index.js

import fetch from 'node-fetch';

class JSONPlaceholder {
  constructor() {
    this.endpoints = {
      posts: {
        list: (options = {}) => {
          return {
            method: 'GET',
            resource: `/posts${options.postId ? `/${options.postId}` : ''}`,
            params: {},
            body: null,
          };
        },
      },
    };
  }
}

export default new JSONPlaceholder();

Než dokončíme náš obal, naším cílem je umět volat koncový bod API, jako je tento:jsonPlaceholder.posts('list') a přijímat odpověď z JSON Placeholder API bez provedení jakýchkoli dalších kroků.

Abychom se tam dostali, potřebujeme standardizovaný způsob generování požadavků HTTP, které budeme provádět. To je to, co děláme výše. Víme, že k provedení požadavku na API budeme potenciálně potřebovat čtyři věci:

  1. Metoda HTTP podporovaná cílovým koncovým bodem (tj. POST , GET , PUT nebo DELETE ).
  2. Zdroj nebo adresa URL pro koncový bod.
  3. Jakékoli volitelné nebo povinné parametry dotazu.
  4. Volitelný nebo povinný objekt těla HTTP.

Zde vytvoříme šablonu pro specifikaci těchto čtyř věcí. Aby byl náš obal uspořádaný, na našem this.endpoints objekt, vytvoříme další vlastnost posts který představuje prostředek API, pro který chceme vygenerovat šablonu požadavku. V tomto rámci přiřazujeme funkce vlastnostem s názvy, které popisují, co požadavek HTTP dělá, a vrací šablonu související s touto úlohou.

Ve výše uvedeném příkladu chceme získat zpět seznam příspěvků. Abychom to udělali, musíme vytvořit šablonu, která nám říká, abychom provedli HTTP GET požadavek na /posts URL v rozhraní JSON Placeholder API. Podmíněně také musíme být schopni předat ID příspěvku tomuto koncovému bodu, jako je /posts/1 nebo /posts/23 .

To je důvod, proč definujeme naše generátory šablon požadavků jako funkce. To nám umožňuje – v případě potřeby – převzít sadu možností předávaných při volání obálky (např. zde chceme převzít ID příspěvku, o kterém předpokládáme, že bude předán přes options.postId ).

Na oplátku z naší funkce získáme zpět objekt, který můžeme později použít v našem kódu k provedení skutečného požadavku HTTP. Rychle, pojďme sestavit zbytek našich generátorů šablon žádostí:

/api/jsonplaceholder/index.js

class JSONPlaceholder {
  constructor() {
    this.endpoints = {
      posts: {
        create: (options = {}) => {
          return {
            method: 'POST',
            resource: `/posts`,
            params: {},
            body: {
              ...options,
            },
          };
        },
        list: (options = {}) => {
          return {
            method: 'GET',
            resource: `/posts${options.postId ? `/${options.postId}` : ''}`,
            params: {},
            body: null,
          };
        },
        post: (options = {}) => {
          if (!options.postId) {
            throw new Error('A postId is required for the posts.post method.');
          }

          return {
            method: 'GET',
            resource: `/posts/${options.postId}`,
            params: {},
            body: null,
          };
        },
        comments: (options = {}) => {
          if (!options.postId) {
            throw new Error('A postId is required for the posts.comments method.');
          }

          return {
            method: 'GET',
            resource: `/posts/${options.postId}/comments`,
            params: {},
            body: null,
          };
        },
      },
    };
  }
}

export default new JSONPlaceholder();

Opakuje se stejný přesný vzor, ​​jen pro různé koncové body a různé účely. Pro každý koncový bod, který chceme podporovat, pod this.endpoints.posts objektu, přidáme funkci přiřazenou k vhodnému názvu, přičemž vložíme možnou sadu options a vrácení šablony požadavku jako objektu se čtyřmi vlastnostmi:method , resource , params a body .

Věnujte velkou pozornost tomu, jak se šablony liší podle koncového bodu. Některé používají jiné method s, zatímco ostatní mají body zatímco ostatní ne. To je to, co jsme mysleli tím, že máme standardizovanou šablonu. Všechny vrátí objekt se stejným tvarem, avšak s tím, co na nastaví tento objekt se liší podle požadavků koncového bodu, ke kterému se snažíme přistupovat.

Měli bychom také upozornit na this.endpoints.posts.post šablona a this.endpoints.posts.comments šablona. Zde vyvoláme chybu, pokud options.postId není definováno, protože ke splnění požadavků těchto koncových bodů je vyžadováno ID příspěvku.

Dále musíme tyto objekty použít. Pamatujte, že naším cílem je dostat se do bodu, kdy můžeme zavolat jsonPlaceholder.posts('list') v našem kódu a získejte zpět seznam příspěvků. Pojďme trochu rozšířit naši třídu o .posts() část tohoto řádku a podívejte se, jak využívá naše šablony požadavků.

/api/jsonplaceholder/index.js

class JSONPlaceholder {
  constructor() {
    this.endpoints = {
      posts: {
        create: (options = {}) => { ... },
        list: (options = {}) => { ... },
        post: (options = {}) => { ... },
        comments: (options = {}) => { ... },
      },
    };
  }

  posts(method = '', options = {}) {
    const existingEndpoint = this.endpoints.posts[method];

    if (existingEndpoint) {
      const endpoint = existingEndpoint(options);
      return this.request(endpoint);
    }
  }
}

export default new JSONPlaceholder();

To by mělo věci trochu objasnit. Zde jsme do našeho JSONPlaceholder přidali metodu třídy posts který přijímá dva argumenty:method a options . První, method , mapuje na jednu z našich šablon, zatímco druhá, options , je místo, kde můžeme podmíněně předávat hodnoty pro náš koncový bod (např. jak jsme viděli dříve u ID příspěvku při definování našich šablon).

Při pohledu na tělo toho posts() metoda, začneme tím, že zkontrolujeme, zda this.endpoints.posts má vlastnost s názvem odpovídajícím předávanému method argument. Pokud například method rovná se list odpověď by byla "ano", ale pokud method rovná se pizza , nebylo by.

Toto je důležité. Nechceme se pokoušet volat kód, který neexistuje. Pomocí proměnné existingEndpoint , pokud na oplátku získáme hodnotu jako existingEndpoint (očekáváme, že se jedná o funkci, pokud je použit platný název), dále chceme tuto funkci zavolat, abychom získali zpět náš objekt šablony požadavku. Všimněte si, že když voláme funkci uloženou v existingEndpoint , předáme v options objekt.

Takže to je jasné, zvažte následující:

jsonPlaceholder.posts('list', { postId: '5' });

Náš obal předávání nazýváme postId nastavte na '5' .

const existingEndpoint = this.endpoints.posts['list'];

Další, protože method byl roven list , dostaneme zpět this.endpoints.posts.list funkce.

(options = {}) => {
  return {
    method: 'GET',
    resource: `/posts${options.postId ? `/${options.postId}` : ''}`,
    params: {},
    body: null,
  };
}

Dále uvnitř této funkce vidíme, že options.postId je definován a vložte jej do adresy URL zdroje jako /posts/5 .

/api/jsonplaceholder/index.js

class JSONPlaceholder {
  constructor() {
    this.endpoints = {
      posts: {
        create: (options = {}) => { ... },
        list: (options = {}) => { ... },
        post: (options = {}) => { ... },
        comments: (options = {}) => { ... },
      },
    };
  }

  posts(method = '', options = {}) {
    const existingEndpoint = this.endpoints.posts[method];

    if (existingEndpoint) {
      const endpoint = existingEndpoint(options);
      return this.request(endpoint);
    }
  }
}

export default new JSONPlaceholder();

Nakonec zpět v našem posts() očekáváme, že dostaneme zpět endpoint což je objekt šablony požadavku, který jsme vygenerovali uvnitř this.endpoints.posts.list .

Dále, těsně pod tím, zavoláme další metodu, kterou musíme definovat:this.request() , předáním endpoint objekt, který jsme obdrželi od this.endpoints.posts.list . Pojďme se nyní na tuto funkci podívat a dokončit náš obal.

/api/jsonplaceholder/index.js

import fetch from 'node-fetch';

class JSONPlaceholder {
  constructor() {
    this.endpoints = {
      posts: {
        create: (options = {}) => { ... },
        list: (options = {}) => { ... },
        post: (options = {}) => { ... },
        comments: (options = {}) => { ... },
      },
    };
  }

  request(endpoint = {}) {
    return fetch(`https://jsonplaceholder.typicode.com${endpoint.resource}`, {
      method: endpoint?.method,
      body: endpoint?.body ? JSON.stringify(endpoint.body) : null,
    }).then(async (response) => {
      const data = await response.json();
      return data;
    }).catch((error) => {
      return error;
    });
  }

  posts(method = '', options = {}) {
    const existingEndpoint = this.endpoints.posts[method];

    if (existingEndpoint) {
      const endpoint = existingEndpoint(options);
      return this.request(endpoint);
    }
  }
}

export default new JSONPlaceholder();

Rychle, než se podíváme na nový request() metoda, nahoře, všimněte si, že jsme jako závislost přidali balíček NPM:node-fetch . Než budeme pokračovat, nainstalujme si to do naší aplikace:

Terminál

npm i node-fetch

Dále se podívejme blíže na tento request() metoda:

/api/jsonplaceholder/index.js

import fetch from 'node-fetch';

class JSONPlaceholder {
  constructor() {
    this.endpoints = {
      posts: {
        create: (options = {}) => { ... },
        list: (options = {}) => { ... },
        post: (options = {}) => { ... },
        comments: (options = {}) => { ... },
      },
    };
  }

  request(endpoint = {}) {
    return fetch(`https://jsonplaceholder.typicode.com${endpoint.resource}`, {
      method: endpoint?.method,
      body: endpoint?.body ? JSON.stringify(endpoint.body) : null,
    }).then(async (response) => {
      const data = await response.json();
      return data;
    }).catch((error) => {
      return error;
    });
  }

  posts(method = '', options = {}) {
    const existingEndpoint = this.endpoints.posts[method];

    if (existingEndpoint) {
      const endpoint = existingEndpoint(options);
      return this.request(endpoint);
    }
  }
}

export default new JSONPlaceholder();

Nyní k té zábavnější části. Uvnitř request() naším cílem je převzít objekt šablony požadavku jako endpoint a použít to k přizpůsobení požadavku HTTP, který provádíme, na rozhraní JSON Placeholder API.

Při pohledu na tuto metodu jsme return volání na fetch metoda, kterou importujeme z node-fetch balíček, který jsme právě nainstalovali. Předáme mu adresu URL, na kterou chceme odeslat požadavek HTTP. Zde je „základní“ adresa URL pro rozhraní API https://jsonplaceholder.typicode.com . Pomocí interpolace řetězců v JavaScriptu (označené zpětnými znaménky, které používáme k definování našeho řetězce na rozdíl od jednoduchých nebo dvojitých uvozovek), zkombinujeme tuto základní adresu URL s endpoint.resource hodnota šablony odpovídající volání.

Pokud bychom například zavolali na jsonPlaceholder.posts('list') očekávali bychom adresu URL, kterou předáme fetch() být https://jsonplaceholder.typicode.com/posts . Pokud bychom zavolali na jsonPlaceholder.posts('list', { postId: '5' }) , očekávali bychom, že adresa URL bude https://jsonplaceholder.typicode.com/posts/5 .

Podle této logiky předáme za URL objekt do fetch() obsahující další možnosti pro požadavek. Zde používáme .method vlastnost na předané šabloně a podmíněně .body vlastnost na předané šabloně. Pokud .body je definován, vezmeme hodnotu, kterou obsahuje, a předáme ji JSON.stringify() —vestavěná funkce JavaScriptu — pro převod objektu na řetězec (důležité, protože hodnotu řetězce můžeme předat pouze pro tělo požadavku HTTP – nikoli pro nezpracovaný objekt).

Poté, na konci našeho volání na fetch() řetězíme .then() funkce zpětného volání, jak očekáváme fetch() vrátit příslib JavaScriptu. Na .then() předáme naši funkci zpětného volání a předpíšeme async klíčové slovo JavaScriptu sdělit, že „chceme použít await klíčové slovo pro jednu z funkcí, které voláme uvnitř této funkce" (bez toho by JavaScript vyvolal chybu s nápisem await bylo vyhrazené klíčové slovo).

Použití response předané této funkci zpětného volání – toto je odpověď HTTP z rozhraní JSON Placeholder API – voláme na její .json() metoda umístěním await vpředu, jak očekáváme response.json() vrátit příslib JavaScriptu. Používáme .json() zde, protože chceme převést prostý text HTTP response body dostaneme z API zpět do dat JSON, která můžeme použít v našem kódu.

Uložení tohoto výsledku do data vrátíme ji z .then() zpětné volání, které se vrátí zpět na return příkaz před fetch() a pak ještě jednou probublávejte zpět na return příkaz před this.request() uvnitř posts() metoda (odkud pochází naše volání). To zase znamená, že očekáváme, že dostaneme naše data vyskočí takto:

const data = await jsonPlaceholder.posts('list');
console.log(data);
/*
[
  {
    "userId": 1,
    "id": 1,
    "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
    "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
  },
  {
    "userId": 1,
    "id": 2,
    "title": "qui est esse",
    "body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"
  },
  {
    "userId": 1,
    "id": 3,
    "title": "ea molestias quasi exercitationem repellat qui ipsa sit aut",
    "body": "et iusto sed quo iure\nvoluptatem occaecati omnis eligendi aut ad\nvoluptatem doloribus vel accusantium quis pariatur\nmolestiae porro eius odio et labore et velit aut"
  },
]
*/

To platí pro náš obal. Nyní, abychom to viděli v akci, zapojíme několik testovacích tras, ke kterým můžeme přistupovat prostřednictvím webového prohlížeče, a zavoláme náš obal, abychom ověřili odpovědi.

Definování tras pro testování obalu

Abychom otestovali náš obal API, nyní zapojíme některé trasy do naší vlastní aplikace, která bude volat rozhraní API JSON Placeholder přes náš obal a poté zobrazí data, která získáme zpět v našem prohlížeči.

/index.server.js

import node from "@joystick.js/node";
import api from "./api";
import jsonPlaceholder from "./api/jsonplaceholder";

node.app({
  api,
  routes: {
    "/": (req, res) => {
      res.render("ui/pages/index/index.js", {
        layout: "ui/layouts/app/index.js",
      });
    },
    "/posts/create": async (req, res) => {
      const post = await jsonPlaceholder.posts('create', { title: 'Testing Posts' });
      res.setHeader('Content-Type', 'application/json');
      res.send(JSON.stringify(post, null, 2));
    },
    "/posts": async (req, res) => {
      const posts = await jsonPlaceholder.posts('list');
      res.setHeader('Content-Type', 'application/json');
      res.send(JSON.stringify(posts, null, 2));
    },
    "/posts/:postId": async (req, res) => {
      const post = await jsonPlaceholder.posts('post', { postId: req?.params?.postId });
      res.setHeader('Content-Type', 'application/json');
      res.send(JSON.stringify(post, null, 2));
    },
    "/posts/:postId/comments": async (req, res) => {
      const comments = await jsonPlaceholder.posts('comments', { postId: req?.params?.postId });
      res.setHeader('Content-Type', 'application/json');
      res.send(JSON.stringify(comments, null, 2));
    },
    "*": (req, res) => {
      res.render("ui/pages/error/index.js", {
        layout: "ui/layouts/app/index.js",
        props: {
          statusCode: 404,
        },
      });
    },
  },
});

Může se to zdát ohromující, ale podívejte se blízko. Uvnitř naší aplikace, když jsme spustili joystick create dříve index.server.js byl pro nás nastaven soubor, kde je spuštěn server Node.js pro naši aplikaci. V tomto souboru node.app() nastaví server Express.js v zákulisí a převezme routes objekt předáme k dynamickému generování tras Express.js.

Zde jsme k tomuto objektu přidali několik testovacích tras, z nichž každá odpovídá jedné z metod v našem obalu API. Také v horní části index.server.js , importovali jsme naše jsonPlaceholder wrapper (nezapomeňte, očekáváme, že se jedná o předinicializovanou instanci našeho JSONPlaceholder třída).

Zaměřujeme se na naše trasy, počínaje /posts/create , zde začneme předáním funkce představující náš obslužný program trasy s async klíčové slovo předřazeno (opět to sděluje JavaScriptu, že bychom chtěli použít await klíčové slovo uvnitř funkce, která následuje po deklaraci).

Zde vytvoříme proměnnou post nastavit rovno volání na await jsonPlaceholder.posts('create', { title: 'Testing Posts' }) . Jak jsme se právě dozvěděli, pokud vše funguje dobře, očekáváme, že to vygeneruje šablonu pro náš HTTP požadavek na JSON Placeholder API a poté provede požadavek přes fetch() , která nám vrátí .json() analyzovaná data z odpovědi. Zde tuto odpověď uložíme jako post a pak udělejte dvě věci:

  1. Nastavte HTTP Content-Type záhlaví v odpovědi na naši cestu Express.js na application/json abychom našemu prohlížeči dali najevo, že obsah, který mu posíláme, jsou data JSON.
  2. Odpověď na požadavek na naši trasu pomocí zpřesněné verze našeho posts odpověď (formátovaná pro použití dvou tabulátorů/mezer).

Pokud otevřeme webový prohlížeč, měli bychom při návštěvě http://localhost:2600/posts/create vidět něco takového :

V pohodě, že? Funguje to, jako bychom napsali celý kód, abychom provedli fetch() požadavek uvnitř naší funkce obslužné rutiny trasy, ale volání nám zabralo pouze jeden řádek kódu!

Když se podíváme zblízka na naše trasy výše, všechny fungují zhruba stejně. Všimněte si rozdílů mezi jednotlivými trasami a toho, jak to změní naše volání na jsonPlaceholder.posts() . Podívejte se například na /posts/:postId/comments trasy, zde používáme comments metoda, kterou jsme propojili a která vyžaduje postId předané v objektu opcí našeho wrapper callu. Abychom to předali, zde vytáhneme postId z parametrů naší cesty a předat jej do objektu voleb obálky jako postId . Na oplátku dostaneme zpět komentáře k příspěvku odpovídající ID, které zadáme v naší URL:

Úžasný. Rychle, pojďme si živě projít všechny naše trasy, než to schválíme:

A tady to máme. Plně funkční API obal. Na tomto vzoru je skvělé, že jej můžeme použít na jakýkoli HTTP nebo REST API, jehož používání bychom rádi standardizovali.

Zabalení

V tomto tutoriálu jsme se naučili, jak vytvořit obal API pomocí třídy Javascript. Napsali jsme náš obal pro JSON Placeholder API, naučili jsme se, jak používat přístup založený na šablonách pro generování požadavků, a jak využít jedinou funkci k provedení tohoto požadavku prostřednictvím fetch() . Také jsme se naučili, jak definovat metody specifické pro zdroje v naší třídě, aby byl náš obal rozšiřitelný a snadno použitelný.