My favorite mistake to make when I'm in a hurry is to think that I have to build new features the sloppiest way possible. Find the quick fix and move on.
That's when things like this happen:
function controller(req, res) {
const {
body: { reason, idempotencyKey },
} = req
const {
body: { charge },
} = req
// ...
}
The code uses JavaScript object destructuring to pull the reason
, idempotencyKey
, and charge
properties out of request body and assign them to local variables. Why I wrote this as two separate blocks of code is anyone's guess.
This is known as a Big Ball of Mud – code with no coherent design. It happens when you keep adding changes without rethinking how the problem changed over time. Common with quick bug fixes and big scary systems you're afraid to break.
How small sensible steps make messy code
Say Alice writes a feature to fetch a list of articles from an internal API. It talks to another micro service.
async function getArticles() {
const res = await fetch(process.env.ARTICLES_URL + "/articles")
if (!res.ok) {
throw new Error("Failed to get articles")
}
return res.json()
}
The code takes an environment variable and adds the /articles
to construct the URL. Then runs a fetch()
request and throws error on a bad response, returns parsed JSON on success.
Seems reasonable.
Missed a non functional requirement
Oh but Alice forgot that the platform team made authentication mandatory. Nobody noticed this code hiding on a branch until you merged and it didn't work.
You quickly add authentication to unblock testing. The way you've seen it done elsewhere.
async function getArticles() {
const res = await fetch(
process.env.ARTICLES_URL + '/articles',
headers: {
...getAuthHeaders()
}
)
if (!res.ok) {
throw new Error("Failed to get articles")
}
return res.json()
}
The getAuthHeaders
function returns all necessary headers for authentication. You never looked into how it works, but it's worked in the past. All good.
Not quite the right functionality
After testing Bob realizes that listing all articles is silly. What people want are their articles.
The API supports user filtering through a URL param. Bob adds userId
to the request:
async function getArticles(userId) {
const res = await fetch(
process.env.ARTICLES_URL + '/' + userId + '/articles',
headers: {
...getAuthHeaders()
}
)
if (!res.ok) {
throw new Error("Failed to get articles")
}
return res.json()
}
Oh wait, that should be optional. Sometimes we do want all articles, like for the admin interface.
async function getArticles(userId) {
const res = await fetch(
process.env.ARTICLES_URL + (userId ? '/' + userId | '') + '/articles',
headers: {
...getAuthHeaders()
}
)
if (!res.ok) {
throw new Error("Failed to get articles")
}
return res.json()
}
Great. A quick ternary expression and the userId
portion of the URL is optional.
Expanding requirements
Now wouldn't it be great, if we could limit articles by time?
Carol is on it! She doesn't quite know what all the code is doing, but she can add a query. That much she's sure of.
async function getArticles(userId, startDate, endDate) {
const res = await fetch(
process.env.ARTICLES_URL + (userId ? '/' + userId | '') + '/articles' + (startDate && endDate ? `?startDate=${startDate}&endDate=${endDate}` : ''),
headers: {
...getAuthHeaders()
}
)
if (!res.ok) {
throw new Error("Failed to get articles")
}
return res.json()
}
If startDate
and endDate
are defined, add a URL fragment with query params.
Rethink your approach when the code gets messy
Alice wants to add a sorting param and pagination and wouldn't search be nice?
She walks into this function and whoa what happened here? It's a mess! That URL construction will break anyone's brain. Not to mention the subtle security bugs.
PS: Query and path concatenation are a common source of security bugs. If values come from user input, they can break out of bounds and inject any URL fragment. This can even lead to reading other users' data!
Alice has been coding for a while and doesn't like clever code anymore. Makes her think too much. She thinks code should be simple and obvious.
Instead of jamming another bug fix into the mess, she takes 10 minutes to rewrite the function.
async function getArticles(userId, startDate, endDate) {
const url = new URL(process.env.ARTICLES_URL)
if (userId) {
url.pathname = `/${userId}/articles`
} else {
url.pathname = '/articles'
}
const params = new URLSearchParams()
if (startDate && endDate) {
params.append('startDate', startDate)
params.append('endDate', endDate)
}
// TODO: add more query params
url.search = params
const res = await fetch(
url.toString(),
headers: {
...getAuthHeaders()
}
)
if (!res.ok) {
throw new Error("Failed to get articles")
}
return res.json()
}
Alice used the URL and URLSearchParams constructors built into JavaScript. Most languages have something similar.
Avoid feeling smart
This code won't make anyone feel smart and that's the point.
It gets the job done, it's readable, and using the standard library means common security bugs and subtle issues are handled without thinking about it.
Plus the code is easy to extend. Anyone can jump in to add more params without making the code messier.
Instead of 2 minutes to hack the code and 30 minutes to debug why it isn't working, Alice spent:
- 10 minutes to clean the function,
- 2 minutes to add new params,
- 5 minutes to test
With more time and a global perspective, someone can turn this approach into a library for everyone to use. Later, when it's an established pattern.
Cheers,
~Swizec
PS: I guess this why firefighters, racing drivers, and others say "Slow is smooth and smooth is fast"
Continue reading about You don't *have to* build it sloppy to go fast
Semantically similar articles hand-picked by GPT-4
- 90% of performance is data access patterns
- How to waste hours of life with fetch() and a bit of brainfart
- Words that scare developers
- My biggest React App performance boost was a backend change
- How GraphQL blows REST out of the water
Learned something new?
Read more Software Engineering Lessons from Production
I write articles with real insight into the career and skills of a modern software engineer. "Raw and honest from the heart!" as one reader described them. Fueled by lessons learned over 20 years of building production code for side-projects, small businesses, and hyper growth startups. Both successful and not.
Subscribe below 👇
Software Engineering Lessons from Production
Join Swizec's Newsletter and get insightful emails 💌 on mindsets, tactics, and technical skills for your career. Real lessons from building production software. No bullshit.
"Man, love your simple writing! Yours is the only newsletter I open and only blog that I give a fuck to read & scroll till the end. And wow always take away lessons with me. Inspiring! And very relatable. 👌"
Have a burning question that you think I can answer? Hit me up on twitter and I'll do my best.
Who am I and who do I help? I'm Swizec Teller and I turn coders into engineers with "Raw and honest from the heart!" writing. No bullshit. Real insights into the career and skills of a modern software engineer.
Want to become a true senior engineer? Take ownership, have autonomy, and be a force multiplier on your team. The Senior Engineer Mindset ebook can help 👉 swizec.com/senior-mindset. These are the shifts in mindset that unlocked my career.
Curious about Serverless and the modern backend? Check out Serverless Handbook, for frontend engineers 👉 ServerlessHandbook.dev
Want to Stop copy pasting D3 examples and create data visualizations of your own? Learn how to build scalable dataviz React components your whole team can understand with React for Data Visualization
Want to get my best emails on JavaScript, React, Serverless, Fullstack Web, or Indie Hacking? Check out swizec.com/collections
Did someone amazing share this letter with you? Wonderful! You can sign up for my weekly letters for software engineers on their path to greatness, here: swizec.com/blog
Want to brush up on your modern JavaScript syntax? Check out my interactive cheatsheet: es6cheatsheet.com
By the way, just in case no one has told you it yet today: I love and appreciate you for who you are ❤️