-
Notifications
You must be signed in to change notification settings - Fork 90
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
1 parent
61bc9f2
commit b478bb2
Showing
17 changed files
with
1,169 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
.env | ||
.git | ||
.gitignore | ||
.dockerignore | ||
node_modules | ||
Dockerfile |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# PokéAPI Proxy |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
Oops, something went wrong.