Skip to content

Commit

Permalink
feat: add PokéAPI proxy (#465)
Browse files Browse the repository at this point in the history
* feat: get basic proxy working

* fix: refactor project to something closer to domain driven design

* fix: set default CACHE_TTL_MINUTES in sample.env

* feat: add content and styling for landing proxy landing page

* feat: dockerize PokéAPI proxy

* fix: use hours to set ttl for caching, update sample env vars

* feat: add note to landing page about the format for pokémon with sex symbols as part of their name

* feat: add middleware and utility function cache and validate all pokémon names and ids served by PokéAPI

* feat: cache by id and name whenever fetching a valid pokémon from PokéAPI

* fix: simplify middleware and error handling, prettify code

* feat: add route to get all pokemon names and routes, refactor to improve caching

* feat: add ids to the list of all valid pokemon

* feat: add /pokemon route description and examples to the landing page

* feat: update README.md

* fix: add Dockerfile, set TTL env var

* fix: rename function to get all resources from /pokemon endpoint
  • Loading branch information
scissorsneedfoodtoo authored Oct 18, 2023
1 parent 61bc9f2 commit b478bb2
Show file tree
Hide file tree
Showing 17 changed files with 1,169 additions and 0 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,11 @@

### Main Curriculum

- PokéAPI Proxy

- [Project description](https://www.freecodecamp.org/learn/2022/javascript-algorithms-and-data-structures/pokemon-search-app-project/build-a-pokemon-search-app)
- [Landing page](https://pokeapi-proxy.freecodecamp.rocks/)

- Stock Price Checker Proxy
- [Project description](https://www.freecodecamp.org/learn/information-security/information-security-projects/stock-price-checker)
- [Landing page](https://stock-price-checker-proxy.freecodecamp.rocks/)
Expand Down
6 changes: 6 additions & 0 deletions apps/pokeapi-proxy/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.env
.git
.gitignore
.dockerignore
node_modules
Dockerfile
14 changes: 14 additions & 0 deletions apps/pokeapi-proxy/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
FROM node:18-bullseye-slim

WORKDIR /app

# Copy over all the files in the project directory to /app early
# for rollup bundling
COPY . .

ENV PORT=3000
ENV CACHE_TTL_HOURS=${POKEAPI_PROXY_CACHE_TTL_HOURS}

RUN npm ci

CMD ["npm", "start"]
1 change: 1 addition & 0 deletions apps/pokeapi-proxy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# PokéAPI Proxy
97 changes: 97 additions & 0 deletions apps/pokeapi-proxy/api/pokemon/pokemon.handlers.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import axios from 'axios';
import { getCache, setCache } from '../utils/cache.mjs';

export const getPokemonEndpointResources = async (req, res, next) => {
try {
const { pokemonIdOrName } = req.params;
// Attempt to get all resources for the Pokémon endpoint from the cache
let pokemonEndpointResources = getCache('pokemonEndpointResources');

if (!pokemonEndpointResources) {
console.log(
'Fetching all resources for the Pokémon endpoint from PokéAPI'
);
const { data } = await axios.get(
`https://pokeapi.co/api/v2/pokemon/?limit=9000`
);
const { count, results } = data;

pokemonEndpointResources = {
count,
results: results.map(obj => {
const { name, url } = obj;
return {
id: Number(url.split('/').filter(Boolean).pop()),
name,
url: url.replace(
'https://pokeapi.co/api/v2/',
`${req.protocol}://${req.get('host')}/api/`
)
};
})
};

// Cache all Pokémon names and routes
setCache('pokemonEndpointResources', pokemonEndpointResources);
}

if (pokemonIdOrName) {
// User is requesting a specific Pokémon, so pass the data to the next middleware
// for id or name validation
res.locals.pokemonEndpointResources = pokemonEndpointResources;
next();
} else {
// User is requesting all Pokémon names and routes, so send the data as a response
res.send(pokemonEndpointResources);
}
} catch (err) {
next(err);
}
};

export const getPokemonData = async (req, res, next) => {
try {
const { pokemonIdOrName } = req.params;
console.log('Fetching Pokémon data from PokéAPI');
const { data } = await axios.get(
`https://pokeapi.co/api/v2/pokemon/${pokemonIdOrName}`
);
const {
base_experience,
height,
id,
name,
order,
sprites,
stats,
types,
weight
} = data;

// Remove unnecessary data for the required project
const simplifiedPokemonData = {
base_experience,
height,
id,
name,
order,
sprites: Object.keys(sprites)
.filter(key => typeof sprites[key] === 'string')
.reduce((obj, key) => {
obj[key] = sprites[key];
return obj;
}, {}),
stats,
types,
weight
};

// Cache simplified data by id and name, then send it as a response
setCache(simplifiedPokemonData.id, simplifiedPokemonData);
setCache(simplifiedPokemonData.name, simplifiedPokemonData);

res.send(simplifiedPokemonData);
} catch (err) {
next(err);
}
};
45 changes: 45 additions & 0 deletions apps/pokeapi-proxy/api/pokemon/pokemon.middleware.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { getCache } from '../utils/cache.mjs';

export const checkCache = (req, res, next) => {
const { pokemonIdOrName } = req.params;

try {
const cachedData = getCache(pokemonIdOrName || 'pokemonEndpointResources');

if (cachedData) {
console.log('Serving cached data');
return res.send(cachedData);
}

next();
} catch (err) {
next(err);
}
};

export const validateNameOrId = async (req, res, next) => {
try {
const { pokemonIdOrName } = req.params;
const validNamesAndIds = res.locals.pokemonEndpointResources.results.reduce(
(arr, currObj) => {
arr.push(currObj.name);
arr.push(currObj.url.split('/').filter(Boolean).pop());
return arr;
},
[]
);

if (validNamesAndIds.includes(pokemonIdOrName)) {
next();
} else {
// Set custom error status code and message
const invalidPokemonErr = new Error();
invalidPokemonErr.statusCode = 404;
invalidPokemonErr.message = 'Invalid Pokémon name or id';

throw invalidPokemonErr;
}
} catch (err) {
next(err);
}
};
19 changes: 19 additions & 0 deletions apps/pokeapi-proxy/api/pokemon/pokemon.routes.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {
getPokemonEndpointResources,
getPokemonData
} from './pokemon.handlers.mjs';
import { checkCache, validateNameOrId } from './pokemon.middleware.mjs';
import express from 'express';
const router = express.Router();

router.get('/pokemon', checkCache, getPokemonEndpointResources);

router.get(
'/pokemon/:pokemonIdOrName',
checkCache,
getPokemonEndpointResources,
validateNameOrId,
getPokemonData
);

export { router };
9 changes: 9 additions & 0 deletions apps/pokeapi-proxy/api/utils/cache.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import NodeCache from 'node-cache';
const cache = new NodeCache({
stdTTL: process.env.CACHE_TTL_HOURS * 3600, // Convert hours to seconds
checkperiod: 120
});

export const getCache = key => cache.get(key);

export const setCache = (key, data) => cache.set(key, data);
Loading

0 comments on commit b478bb2

Please sign in to comment.