promote reusability
Try not to put too much into your classes. Functions should be designed around portability, making them easier to resuse and test.
function getJson(url) {
return fetch(url)
.then((response) => response.text())
.then((data) => JSON.parse(data))
}
Now the class can stay lean and any other part of your program that needs JSON can benefit from the separated function as well -
class MyClass {
constructor () {
// ...
}
async getContent () {
const data = await getJson(this.url)
// ...
}
}
Moving onto fetchPokemon
, look at that, we need some more JSON! Fetching a single Pokemon is a simple task that requires only an input url
and returns a new Pokemon
. There's no reason to tangle it with class logic -
async function fetchPokemon(url) {
const { name, types, weight, sprites } = await getJson(url)
return new Pokemon(
name,
types.map(t => t.name),
weight,
sprites.front_default
)
}
Now we can finish the getContent
method of our class -
class MyClass {
constructor () {
// ...
}
async getContent () {
const { results } = await getJson(this.url)
return Promise.all(results.map(item => fetchPokemon(item.url)))
}
}
veil lifted
Now we can plainly see that getContent
is nothing more than a simple function that requests multiple pokemon. If it were mere, I would also make this a generic function -
async function fetchAllPokemon(url) {
const { results } = await getJson(url)
return Promise.all(results.map(item => fetchPokemon(item.url)))
}
The need for your class has essentially vanished. You haven't shared the other parts of it, but your program has simplified to three (3) functions
getJson
, fetchPokemon
and fetchAllPokemon
all of which are reusable and easy to test, and a dead-simple MyClass
wrapper -
function getJson(...) { ... }
async function fetchPokemon(...) { ... }
async function fetchAllPokemon(...) { ... }
class MyClass {
constructor() {
// ...
}
getContent() {
return fetchAllPokemon(this.url)
}
}
promote versatility
I should also mention that getJson
can be simplified, as the fetch
response includes a .json
method. To make it even more useful, you can add a second options
parameter -
function getJson(url, options = {}) {
return fetch(url, options).then(res => res.json())
}
don't swallow errors
Finally don't attach .catch
error handlers on your generic functions or class methods. Errors will automatically bubble up and reserves opportunity for the caller to do something more meaningful -
fetchAllPokemon(someUrl)
.then(displayResult)
.catch(handleError)
Or if you still plan on using a class -
const foobar = new MyClass(...)
foobar
.getContent()
.then(displayResult)
.catch(handlerError)
zoom out
Historically classes were used to organize your program by encapsulating logic, variables, and methods. Often times class structures result in hard-to-manage and difficult-to-test hierarchies. Nowadays with module support via import
and export
, you can write programs that are "flat" and functions and components can be combined in flexible ways. Consider this ExpressJS example using the functions we wrote above -
import express from "express"
import { fetchPokemon, fetchAllPokemon } from "./api.js"
app.get("/pokemon", (res, res, next) => {
fetchAllPokemon(`https://someurl/`)
.then(data => res.json(data))
.then(_ => next())
.catch(err => next(err))
}
app.get("/pokemon/:id",(req,res,next)=> {
fetchPokemon(`https://someurl/${req.params.id}`)
.then(data => res.json(data))
.then(_ => next())
.catch(err => next(err))
}
Or even better with async
and await
, as errors automatically bubble up -
import express from "express"
import { fetchPokemon, fetchAllPokemon } from "./api.js"
app.get("/pokemon", async (res, res) => {
const data = await fetchAllPokemon(`https://someurl/`)
res.json(data)
}
app.get("/pokemon/:id", async (res, res) => {
const data = await fetchPokemon(`https://someurl/${req.params.id}`)
res.json(data)
}