Skip to content

Commit b478bb2

Browse files
feat: add PokéAPI proxy (#465)
* 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
1 parent 61bc9f2 commit b478bb2

File tree

17 files changed

+1169
-0
lines changed

17 files changed

+1169
-0
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,11 @@
180180

181181
### Main Curriculum
182182

183+
- PokéAPI Proxy
184+
185+
- [Project description](https://www.freecodecamp.org/learn/2022/javascript-algorithms-and-data-structures/pokemon-search-app-project/build-a-pokemon-search-app)
186+
- [Landing page](https://pokeapi-proxy.freecodecamp.rocks/)
187+
183188
- Stock Price Checker Proxy
184189
- [Project description](https://www.freecodecamp.org/learn/information-security/information-security-projects/stock-price-checker)
185190
- [Landing page](https://stock-price-checker-proxy.freecodecamp.rocks/)

apps/pokeapi-proxy/.dockerignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.env
2+
.git
3+
.gitignore
4+
.dockerignore
5+
node_modules
6+
Dockerfile

apps/pokeapi-proxy/Dockerfile

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
FROM node:18-bullseye-slim
2+
3+
WORKDIR /app
4+
5+
# Copy over all the files in the project directory to /app early
6+
# for rollup bundling
7+
COPY . .
8+
9+
ENV PORT=3000
10+
ENV CACHE_TTL_HOURS=${POKEAPI_PROXY_CACHE_TTL_HOURS}
11+
12+
RUN npm ci
13+
14+
CMD ["npm", "start"]

apps/pokeapi-proxy/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# PokéAPI Proxy
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import axios from 'axios';
2+
import { getCache, setCache } from '../utils/cache.mjs';
3+
4+
export const getPokemonEndpointResources = async (req, res, next) => {
5+
try {
6+
const { pokemonIdOrName } = req.params;
7+
// Attempt to get all resources for the Pokémon endpoint from the cache
8+
let pokemonEndpointResources = getCache('pokemonEndpointResources');
9+
10+
if (!pokemonEndpointResources) {
11+
console.log(
12+
'Fetching all resources for the Pokémon endpoint from PokéAPI'
13+
);
14+
const { data } = await axios.get(
15+
`https://pokeapi.co/api/v2/pokemon/?limit=9000`
16+
);
17+
const { count, results } = data;
18+
19+
pokemonEndpointResources = {
20+
count,
21+
results: results.map(obj => {
22+
const { name, url } = obj;
23+
return {
24+
id: Number(url.split('/').filter(Boolean).pop()),
25+
name,
26+
url: url.replace(
27+
'https://pokeapi.co/api/v2/',
28+
`${req.protocol}://${req.get('host')}/api/`
29+
)
30+
};
31+
})
32+
};
33+
34+
// Cache all Pokémon names and routes
35+
setCache('pokemonEndpointResources', pokemonEndpointResources);
36+
}
37+
38+
if (pokemonIdOrName) {
39+
// User is requesting a specific Pokémon, so pass the data to the next middleware
40+
// for id or name validation
41+
res.locals.pokemonEndpointResources = pokemonEndpointResources;
42+
next();
43+
} else {
44+
// User is requesting all Pokémon names and routes, so send the data as a response
45+
res.send(pokemonEndpointResources);
46+
}
47+
} catch (err) {
48+
next(err);
49+
}
50+
};
51+
52+
export const getPokemonData = async (req, res, next) => {
53+
try {
54+
const { pokemonIdOrName } = req.params;
55+
console.log('Fetching Pokémon data from PokéAPI');
56+
const { data } = await axios.get(
57+
`https://pokeapi.co/api/v2/pokemon/${pokemonIdOrName}`
58+
);
59+
const {
60+
base_experience,
61+
height,
62+
id,
63+
name,
64+
order,
65+
sprites,
66+
stats,
67+
types,
68+
weight
69+
} = data;
70+
71+
// Remove unnecessary data for the required project
72+
const simplifiedPokemonData = {
73+
base_experience,
74+
height,
75+
id,
76+
name,
77+
order,
78+
sprites: Object.keys(sprites)
79+
.filter(key => typeof sprites[key] === 'string')
80+
.reduce((obj, key) => {
81+
obj[key] = sprites[key];
82+
return obj;
83+
}, {}),
84+
stats,
85+
types,
86+
weight
87+
};
88+
89+
// Cache simplified data by id and name, then send it as a response
90+
setCache(simplifiedPokemonData.id, simplifiedPokemonData);
91+
setCache(simplifiedPokemonData.name, simplifiedPokemonData);
92+
93+
res.send(simplifiedPokemonData);
94+
} catch (err) {
95+
next(err);
96+
}
97+
};
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { getCache } from '../utils/cache.mjs';
2+
3+
export const checkCache = (req, res, next) => {
4+
const { pokemonIdOrName } = req.params;
5+
6+
try {
7+
const cachedData = getCache(pokemonIdOrName || 'pokemonEndpointResources');
8+
9+
if (cachedData) {
10+
console.log('Serving cached data');
11+
return res.send(cachedData);
12+
}
13+
14+
next();
15+
} catch (err) {
16+
next(err);
17+
}
18+
};
19+
20+
export const validateNameOrId = async (req, res, next) => {
21+
try {
22+
const { pokemonIdOrName } = req.params;
23+
const validNamesAndIds = res.locals.pokemonEndpointResources.results.reduce(
24+
(arr, currObj) => {
25+
arr.push(currObj.name);
26+
arr.push(currObj.url.split('/').filter(Boolean).pop());
27+
return arr;
28+
},
29+
[]
30+
);
31+
32+
if (validNamesAndIds.includes(pokemonIdOrName)) {
33+
next();
34+
} else {
35+
// Set custom error status code and message
36+
const invalidPokemonErr = new Error();
37+
invalidPokemonErr.statusCode = 404;
38+
invalidPokemonErr.message = 'Invalid Pokémon name or id';
39+
40+
throw invalidPokemonErr;
41+
}
42+
} catch (err) {
43+
next(err);
44+
}
45+
};
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import {
2+
getPokemonEndpointResources,
3+
getPokemonData
4+
} from './pokemon.handlers.mjs';
5+
import { checkCache, validateNameOrId } from './pokemon.middleware.mjs';
6+
import express from 'express';
7+
const router = express.Router();
8+
9+
router.get('/pokemon', checkCache, getPokemonEndpointResources);
10+
11+
router.get(
12+
'/pokemon/:pokemonIdOrName',
13+
checkCache,
14+
getPokemonEndpointResources,
15+
validateNameOrId,
16+
getPokemonData
17+
);
18+
19+
export { router };
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import NodeCache from 'node-cache';
2+
const cache = new NodeCache({
3+
stdTTL: process.env.CACHE_TTL_HOURS * 3600, // Convert hours to seconds
4+
checkperiod: 120
5+
});
6+
7+
export const getCache = key => cache.get(key);
8+
9+
export const setCache = (key, data) => cache.set(key, data);

0 commit comments

Comments
 (0)