@rogwilco/pokedex-client
v1.0.0-rc.1
Published
A TypeScript SDK for interacting with the [PokéAPI](https://pokeapi.co).
Downloads
10
Readme
Pokédex Client
A TypeScript SDK for interacting with the PokéAPI.
Installation
To get started, simply install the SDK using your package manager of choice:
- NPM
npm install --save @rogwilco/pokedex-client
- Yarn
yarn add @rogwilco/pokedex-client
- PNPM
pnpm add @rogwilco/pokedex-client
Usage
Initializing the Client
import { PokedexClient } from '@rogwilco/pokedex-client'
// Create a new client instance
const client = new PokedexClient()
Fetching Resources
All supported root-level resources can be fetched by accessing their
corresponding root-level property on the Pokédex client instance:
client.<resource>.<method>()
.
Examples:
// Fetch the first 10 Pokémon
const firstPageOfPokemon = await client.pokemon.getMany(10)
// Fetch the first 3 generations
const generations = await client.generations.getMany(3)
Supported Operations
<resource>.getMany(:limit, :offset)
Fetches a list of resources, with optional pagination parameters.
Parameters:
limit
(number, optional): The maximum number of resources to fetch. Defaults to20
.offset
(number, optional): The zero-based starting index of the resources to fetch. Defaults to0
.
Returns: Promise<Resource[]>
Example:
// Fetch the first 20 Pokémon
const firstPageOfPokemon = await client.pokemon.getMany()
// Fetch the first 10 Pokémon, starting with the 6th (zero-based index)
const firstPageOfPokemon = await client.pokemon.getMany(10, 5)
<resource>.getOne(:id | :name)
Fetches a single resource by ID or name.
Parameters:
id
(number, required): The ID of the resource to fetch.name
(string, required): The name of the resource to fetch.
Returns: Promise<Resource>
Example:
// Fetch a single Pokémon by ID
const pikachu = await client.pokemon.getOne(25)
// Fetch a single Pokémon by name
const bulbasaur = await client.pokemon.getOne('bulbasaur')
<resource>.getAll()
Fetches all resources of a given type.
WARNING: This method will invoke as many paginated requests as required to fetch everything and may be rate-limited.
Returns: Promise<Resource[]>
Example:
// Fetch all generations
const allGenerations = await client.generations.getAll()
<resource>.count()
Fetches the total count of all resources of a given type.
Returns: Promise<number>
Example:
// Fetch the total count of all Pokémon
const totalPokemonCount = await client.pokemon.count()
Contributing
Adding a New Resource
New resources can be added using a relatively simple process. Consider the following example for adding a new resource named Ability
:
- Identify the
getMany()
endpoint URL for the desired resource:https://pokeapi.co/api/v2/ability
- The SDK framework will automatically prepend the base URL (
https://pokeapi.co/api/v2
) to the endpoint URL. - The
getOne()
endpoint URL is derived from thegetMany()
URL by appending/:id
to the end.
- The SDK framework will automatically prepend the base URL (
- Create a new file under
./src/resources
named after the desired resource:./src/resources/Ability.ts
- Use the following template as a guide for defining the new resource class and its associated API schemas:
import {
GetMany,
GetOne,
NamedResource,
Nested,
Resource,
ResourceClient,
Transformable,
Transformed,
} from '../lib'
@Resource('/ability') // <-- Use this decorator to define the resource's endpoint relative URL
export class Ability extends NamedResource {
@GetMany() // <-- Use this decorator to indicate this field is populated when calling `getMany()`
@GetOne() // <-- Use this decorator to indicate this field is populated when calling `getOne()`
id?: number;
@GetOne(r => r.name.toLowerCase()) // <-- A custom transformation function can be provided when
name?: string; // the API response doesn't match the desired format or type.
@GetOne<AbilityGetOne>(r => r.names.map(n => n.name)) // <-- If desired, use the decorator's generic
names?: string[]; // parameters to specify the incoming API schema
// ...
}
export interface AbilityGetOne { // <-- Define the API schema for the `getOne()` method
// ...
}
export interface AbilityGetMany { // <-- Define the API schema for the `getMany()` method
// ...
}
- Update the root
index.ts
file, adding the new resource class to the Pokédex Client:
import { Ability } from './resources/Ability';
// ...
// Create a new client using the ResourceClient mixin, explicitly naming the
// class to ensure readable stack traces and easier debugging.
export class AbilityClient extends ResourceClient(Pokemon) {}
export class PokedexClient {
// ...
// Add a new public property to the Pokédex Client.
public ability: AbilityClient
constructor(config?: CreateAxiosDefaults) {
// ...
// Initialize the new resource client inside the constructor.
this.ability = new AbilityClient(resolvedConfig)
}
}
- At this point the new resource should be fully integrated into the SDK and ready for use.
import { PokedexClient } from '@rogwilco/pokedex-client'
const client = new PokedexClient()
// Fetch some abilities
const abilities = await client.ability.getMany()
Testing
Test coverage will be automatically generated when runing any of the test scripts. The coverage report will be saved to ./out/coverage
.
# Run all available tests
yarn test
# Run only unit tests
yarn test:unit
# Run only end-to-end tests
yarn test:e2e
# Run a specific test file
yarn test:unit src/resources/Pokemon.test.ts
Linting and Formatting
# Run ESLint
yarn lint
# Run Prettier and ESLint in fix mode
yarn format
# Run Prettier and ESLint in fix mode for a specific file
yarn format src/resources/Pokemon.ts
Generating Documentation
Source documentation is generated using TypeDoc. The generated documentation will be saved to ./out/docs
.
# Generate Source Code Documentation
yarn docs
Design Notes
Considerations
In this first iteration, the SDK is primarily focused around two things; providing a clean and intuitive interface for SDK consumers, and providing ease of extensibility for SDK contributors.
This came with a few key considerations:
Structure
The SDK is organized according to three main areas of concern:
Internal: The internal library code used by the SDK itself to interact with the API.
- Everything within
./src/lib
- No API-specific logic or references.
- Could easily be carved out into its own package.
- Everything within
Shared: The shared code used by both the internal library and partly exposed through the external SDK interface.
- Everything within
./src/resources
and partially./src/index
- Contains all API-specific logic and references.
- Everything within
External: The public-facing SDK interface that is interacted with by the consuming application or package.
- Everything within
./src/index
- Serves as the point of entry for the SDK.
- Everything within
Performance & Efficiency
Due to the experimental nature of the approach, the SDK is not optimized for performance or efficiency and should not be considered as a solution intended for production use.
While some of this is due to limitations imposed by the design of the API, there are still areas where improvements could be made.
API Limitations
- No support for server-side query filtering or sorting.
- No support for partial responses or field selection.
- No visibility into the rate limits or throttling imposed by the API.
Extensibility
The declarative approach to defining resources and API schemas drew inspiraiton from other popular libaries like TypeORM and TypeGraphQL. For example, in the same way that TypeORM allows developers to define entities and their relationships using decorators, the SDK allows developers to define API resources and uses decorators to define how they are created from API responses.
Future Improvements
Shoring up the performance and efficiency of the SDK would be a top priority for future iterations, as well as expanding the SDK to support a more comprehensive set of API features:
Caching: Implementing a caching mechanism to store resources that have already been fetched. This would prevent unnecessary requests and reduce the load on the API.
Redundant Requests: Optimizing the SDK to reduce the number of request needed for any given operation could be useful.
- This could involve the wholesale removal of the
getAll()
method, which is admittedly a fairly egregious offender. In fact this method should generally be considered an anti-pattern for a production SDK. - The
count()
method could be optimized to prefer the cached count obtained from a previousgetMany()
orcount()
invocation.
- This could involve the wholesale removal of the
Unit Tests: Including traditional unit tests that isolate components and mock dependencies to ensure that each part of the SDK works as expected.
Endpoint Parity: Expanding the SDK to support all available API endpoints and resources. This would involve some refactoring to support linking the various nested relationships between resources.
Logging & Monitoring: Adding logging and performance monitoring would improve visibility into API reqests and responses and help developers debug issues more effectively.
Generic API Client: Separating the library code into its own package would allow the core of the SDK to support any RESTful API, not just the PokéAPI. Once generic, the following improvements could also be made:
Authentication: Providing support for common authentication mechanisms suce as HTTP Auth and token-based schemes such as OAuth2, JWT, etc.
Multiple Protocols: Extending the SDK to support other protocols such as GraphQL, gRPC, etc.