Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
62a3d38
feat: get basic proxy working
scissorsneedfoodtoo Jul 19, 2023
6162aca
fix: refactor project to something closer to domain driven design
scissorsneedfoodtoo Jul 19, 2023
be14527
fix: set default CACHE_TTL_MINUTES in sample.env
scissorsneedfoodtoo Jul 19, 2023
f2287a5
feat: add content and styling for landing proxy landing page
scissorsneedfoodtoo Jul 20, 2023
8d14f4e
feat: dockerize PokéAPI proxy
scissorsneedfoodtoo Jul 21, 2023
b6f410b
fix: use hours to set ttl for caching, update sample env vars
scissorsneedfoodtoo Jul 25, 2023
dc4a470
feat: add note to landing page about the format for pokémon with sex …
scissorsneedfoodtoo Jul 25, 2023
290b375
feat: add middleware and utility function cache and validate all poké…
scissorsneedfoodtoo Jul 25, 2023
add8b6f
feat: cache by id and name whenever fetching a valid pokémon from Pok…
scissorsneedfoodtoo Jul 25, 2023
44b5a55
fix: simplify middleware and error handling, prettify code
scissorsneedfoodtoo Jul 26, 2023
c70dccc
feat: add route to get all pokemon names and routes, refactor to impr…
scissorsneedfoodtoo Sep 19, 2023
d50ffa0
feat: add ids to the list of all valid pokemon
scissorsneedfoodtoo Sep 19, 2023
b148a18
feat: add /pokemon route description and examples to the landing page
scissorsneedfoodtoo Sep 19, 2023
5c45ad6
feat: update README.md
scissorsneedfoodtoo Sep 19, 2023
e340c75
fix: add Dockerfile, set TTL env var
scissorsneedfoodtoo Sep 19, 2023
36ba96e
fix: rename function to get all resources from /pokemon endpoint
scissorsneedfoodtoo Sep 20, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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