Better boundaries in JS

JavaScript code structuring is quite difficult. A lot of frameworks and projects have come up with ways that work for them.

In this post I want to show a new technique that I have used on a side project.

In essence you define one Object for each domain, that clearly encapsulates everything that belongs to its domain. The implementation details are not written in that file though, as it would probably get huge.
Instead the actual implementations will be imported, reduced onto it and optionally get some dependencies injected.

I think some code will describe it best.

Code

Exposer

The top of the file just imports the helpers, or the actual implementations (should be named better).

The interesting part is the const Api.
All of the helpers functions will be reduced onto one object, a check should be implemented so that functions wont be overriden.
Additionally some dependencies will be injected, which makes the implementatios easier, as they can just destructure the first parameter to get the needed data.

import Entries from "./Entries";
import Repos from "./Repos";
import Logger from "../Logger";
import config from "../../../configs/appConfig";

const helpers = [Entries, Repos];

async function get(url) {
  const response = await fetch(url);
  const json = await response.json();
  if (json.statusCode === 500) {
    const error = new Error(`
       Could not GET data from ${url}. \n
       Message from Server: ${json.message}`);

    Logger.error(error);
    throw error;
  }
  return json;
}

async function post(url, data) {
  try {
    const response = await fetch(url, {
      body: JSON.stringify(data),
      headers: {
        "content-type": "application/json"
      },
      method: "POST"
    });
    const json = response.json();
    if (json.statusCode === 500)
      throw new Error(`
       Could not POST to ${url}. \n
       Message from Server: ${json.message}`);
  } catch (e) {
    Logger.error(e);
    return e;
  }
}

const apiConfig = {
  get,
  post,
  config
};

const Api = helpers.reduce((prevHelper, nextHelper) => {
  const keys = Object.keys(nextHelper);
  const wrappedHelper = keys.reduce((prevKey, nextKey) => {
    const nextValue = nextHelper[nextKey];
    if (typeof nextValue === "function") {
      const wrappedFunction = (...args) => nextValue(apiConfig, ...args);
      return Object.assign({}, prevKey, { [nextKey]: wrappedFunction });
    } else {
      return Object.assign({}, prevKey, nextKey);
    }
  }, {});
  return Object.assign({}, prevHelper, wrappedHelper);
}, {});

export default Object.assign(Api, { get, post });

Repos

This is a very simple function that will fetch all the repos. But you can see that the actual get functionality and the config object are injected.

async function getRepos({ get, config }) {
  try {
    const repos = await get(`${config.baseUrl}/repos`);
    return repos;
  } catch (e) {
    return [];
  }
}

export default {
  getRepos
};

Entries

Another simple function with dependency injection, but note the parameter repo. This will be the only one exposed by the API object, since the dependencies are injected via a curried function.

async function getEntries({ get, config }, repo) {
  try {
    const entries = await get(`${config.baseUrl}/entries/${repo}`);
    return entries;
  } catch (e) {
    return [];
  }
}

export default {
  getEntries
};

Conclusion

My believe is, that this can be of alot of help for more junior developers. All code is clearly structured inside its domain and only exposed through one single point.
In essence you are writing small libraries for each project. The good thing is though, that imports are not used in many different places.

Cross boundary imports would always just be on one object for each domain.

This way we have a nicely decoupled architecture inside of our repository, which will make it more accessible and easy to switch out certain pieces when necessary.

This of course isnt revolutionary, but something you might want to consider for a new project.

Back to overview