How I Structure My JavaScript File
Antonin J. (they/them)
Posted on April 6, 2018
Loads of people have been asking me how I write my JavaScript — okay, that’s a lie, no one asks me that but if they did, I’d like to point them to this article. I adopted my code style over the years, after reading Clean Code (and other books), after using years of PHP. Yes, PHP, don’t knock it, it’s got a great community and great coding standards. And of course, years of writing JS with others, and following along styleguides from various companies.
The structure doesn’t depend on JS modules but I tend to write only JS modules these days so I’ll use those.
The structure, in summary is as follows:
//imports
import fs from 'fs';
import utils from 'utils';
import db from '../../../db';
import { validatePath } from './readerHelpers';
// constants
const readDir = utils.promisify(fs.readDir);
const knex = db.knex;
// main exports
export async function fileReader(p) {
validatePath(p);
return await readFile(p);
}
// core logic
function readFile(p) {
// logic
}
The Imports
At the top of the files are imports. That makes sense, they get hoisted above everything else. The order of imports doesn’t matter unless you use some hooks (like a babel hook) so I tend to prefer the structure of:
- native modules — stuff native to Node
- library modules — lodash, knex, whatever
- local libraries — like
../db
- local files — like
./helpers
or similar
Keeping my modules organized makes it easier for me to see what I’m importing and what I’m actually using. I also tend to write dependencies this way as I start writing code.
I tend to not care at all about alphabetizing (other than in destructured imports) and I don’t really see a point in it.
Native Modules
I tend to put native modules all the way on top and keep a clear organization by theme like so:
import path from 'path';
import fs from 'fs';
import util from 'util';
If I’m in the browser, I obviously skip this step.
Library modules
I try to import only what I need from libraries whenever I can, but again, I group them by some theme.
import knex from 'knex';
import { clone } from 'lodash';
I also noticed that if I’m doing a default import (eg. the knex import) I tend to put it at the top of my library modules, and leave my destructured imports lower down. Not necessary but I like how that looks visually.
Local/Internal libraries
By local libraries, I mean locally shared modules like a db.js
file that setups a connection with Bookshelf. Or, in my case at work, we have several libraries that deal with numbers and calculations that are used all across our product.
import db from '../../../db';
import calculators from '../../../lib/calculators';
Local files
Lastly, I import local files that are usually in the same folder as the file I’m working on or one directory up (at most). For example, I wrote a reducer for Redux and have it in a separate folder from other reducers. Inside that folder, I also keep a helper file usually named something like [reducer name]Helpers.js
:
import { assignValue, calculateTotal } from './calculationReducerHelpers';
Constants
After I import all of my dependencies, I usually do some up-front work that’ll be used in the rest of the module. For example, I extract knex
from my Bookshelf
instance. Or I might setup value constants.
const knex = db.knex;
const pathToDir = '../../data-folder/';
Using non-constants usually indicates that I’m depending on some kind of singleton. I try to avoid those but sometimes it’s either necessary because there’s no easy other way to do it, or it doesn’t matter much (such as one-off command line scripts).
Exports
After I basically setup all module-level dependencies: whether they’re constant values or imported libraries, I try to group my exports at the top of the file. Basically, this is where I put the functions that act as glue for the module and that fulfill the ultimate purpose of the module.
In the case of Redux, I might export a single reducer that then splits the work apart and calls the relevant logic. In the case of ExpressJS, I might export all of my routes here while the actual route logic is below.
import { COUNT_SOMETHING } from './calculationActions';
import helpers from './calculationHelpers';
export function calculationReducer(state, action) {
switch (action.type) {
case COUNT_SOMETHING:
return calculateSomething(state, action);
}
}
I’d like to mention that this isn’t the only section where I export functions.
I feel like the way the module system works makes it a little difficult to draw a clear line between exposing the narrowest API possible and also exporting functions to use them in testing.
In the example above, for instance, I’d never want to use calculateSomething
outside of the module. I’m not entirely sure how OOP languages handle testing private functions but it’s a similar problem.
Core Logic
It might seem strange but the core logic goes last for me. I totally understand when people flip exports and core logic but this works well for me for a number of reasons.
When I open a file, the top-level function tells me what will happen in abstract steps. I like that. I like, at a glance, knowing what the file will do. I do a lot of CSV manipulation and insertion into DB and the top-level function is always an easy to understand process that has a flow like: fetchCSV → aggregateData → insertData → terminate script
.
The core logic always encompasses what happens in the exports from top to bottom. So in the inline example, we’d have something like this:
export async function importCSV(csvPath) {
const csv = await readCSV(csvPath);
const data = aggregateData(csv);
return await insertData(data);
}
function aggregateData(csv) {
return csv
.map(row => {
return {
...row,
uuid: uuid(),
created_at: new Date(),
updated_at: new Date(),
};
})
;
}
function insertData(data) {
return knex
.batchInsert('data_table', data)
;
}
Note that readCSV
isn’t there. It sounds generic enough that I would have pulled it out into a helpers file and imported it above instead. Other than that, you can see my export vs. not dilemma again. I wouldn’t want aggregateData
available outside of the module but I’d also still like to test it.
Outside of that, I tend to put “meatier” functions up top and smaller functions below. If I have a module-specific utility function, a function I use in more than one place but only within the module, I’ll place those all the way on the bottom. Basically, I order by: complexity + use.
So the priority the order is:
- core-logic functions — functions used by the top-level exports in order of use
- simpler/smaller functions — functions used by core-logic functions
- utility functions — small functions used in multiple places around the module (but are not exported)
Core-logic functions
Core logic functions are like the “sub-glue” of my exported functions. Depending on the complexity of your module, these may or may not exist. The breakdown of functions isn’t required but if a module grows large enough, the core-logic functions are like the steps in the main function.
If you’re writing something like React or Angular, these your components will be the exported functions I mentioned above. But your core-logic functions will be implementations of various listeners, or data processors. With Express, these will be your specific routes. In a Redux reducer, these will be the individual reducers that are far-enough along the chain to not have a switch/case statement.
If you’re in Angular, it’s totally fair game to organize these functions within the class rather than in the scope of an entire file.
export FormComponent extends Component {
function constructor() { }
onHandleInput($event) {
// logic
}
}
Simpler/Smaller functions
These functions are generally the in-between step for core-logic and pure utility. You might use these once or they might be just a tad more complicated than utility functions. I could probably remove this category and say “write your functions in order of decreasing complexity or amount of work”.
Nothing to mention here. Maybe your onHandleInput
event listener requires some logic to mangle the $event
data so if it’s pure, you might take it out of the class, and if it’s not, you keep it in the class like so:
export FormComponent extends Component {
onHandleInput($event) {
try {
validateFormInput($event);
} catch (e) {
}
}
validateFormInput($event) {
if (this.mode === 'strict-form') {
throw new Error();
}
}
}
Utility functions
Lastly, utility functions. I tend to organize my utilities closest to where I use them. Either within the same file, or the same folder (when necessary), same module, etc. I move the functions out a level every time the usage expands from in-file all the way to the root of the project or its own NPM module.
A utility function, in my mind, should always be a pure method, meaning that it shouldn’t access variables outside of its scope and should rely only on data being passed into it and without side-effects of any kind. Except when using a utility function to hit up an API or accessing DB. Since these are considered side-effects, I’d say they’re the only exception.
function splitDataByType(data) {
return data
.reduce((typeCollection, item) => {
if (!typeCollection[item.type]) {
typeCollection[item.type] = [];
}
typeCollection[item.type].push(item);
return typeCollection;
}, {});
}
function insertData(data, knex) {
return knex
.batchInsert('data', data);
}
Anything else?
Sure! I think everyone has their particular way of writing code. The above-described structure has worked very well for me over the years of writing tons of code every single day. Eventually, a lot of the nuances started to appear and I found myself writing code faster, enjoying it more, and having an easier time debugging and testing.
Before I’m done with this post, I’d like to share a couple of coding tidbits that I’ve grown very accustomed to that have less to do with the document structure and more to do with small preference in writing actual code.
Early returns
When I discovered early returns, it was an immediate lightbulb moment. Why wrap large chunks of code in an else
statement when you can just return early?
My rule of thumb is that if the early return condition is smaller than the remaining code, I’ll write the early return but if it’s not, I’d flip the code upside down so that the smaller code chunk is always the early return.
function categorize(collection, categories) {
return collection.reduce((items, item) => {
if (!categories.includes(item.category) {
return items;
}
if (!items[item.category]) {
items[item.category] = [];
}
items[item.category].push(item);
return items;
}, {});
}
Early returns also work wonderfully in switches and I’m a huge fan of them in Redux.
Semi-colon blocks
Though I don’t use it as much anymore (no Prettier support), I’d always terminate function chaining with a semicolon on a separate line, one indentation to the left of the chain’s indentation. This creates a neat block where code isn’t just left hanging.
Of course, this means that I also prefer using semicolons over not.
return fetchPost(id)
.then(post => processPost(post))
.then(post => updatePost(post, userInput))
.then(post => savePostUpdate(post))
; // <- terminating semicolon
Or better written, it might look like this:
return fetchPost(id)
.then(processPost)
.then(updatePost(userInput))
.then(savePostUpdate)
; // <- terminating semicolon
Posted on April 6, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 10, 2024