Oh no, the blog has fallen to clickbait titles! This must be the end! No worries, it's just that I have a list of tips that don't warrant a single post for each, so grouping them under a single post just made more sense. Let's clickbait in!
How many of these do you already do?
Tick the ones you already practice. The rest link to their section.
0 of 10
Plenty of quick wins ahead. Start with the unchecked ones, each takes minutes to set up.
#Default vs Named Exports
JavaScript wasn't initially designed to allow the concept of modules. All your code shared a single global scope. Yeah, JavaScript wasn't meant to be used at the scale it's used today. Funny, right?
So, once we saw that we actually needed to split things in files to make the code more manageable we invented the concept of the .
;(function () {
var counter = 0
// counter is private to this function. yes, if you use var it's global unless you use strict mode
})()Which in turn we used to hack together the concept of modules:
var counterModule = (function () {
var counter = 0
function increment() {
counter++
}
function get() {
return counter
}
return { increment: increment, get: get }
})()
counterModule.increment()
counterModule.get() // 1To add on top of this, we defined a function to import and an object to export, which was... You guessed it right: require.
// counter.js
var counter = 0
module.exports = {
increment: function () {
counter++
},
}
// main.js
var counter = require('./counter')
counter.increment()So, we invented CommonJS, and Node.js adopted it. I'm telling you all of this because it took a while for the to land a proposal to actually support native modules (they arrived with ES2015).
Since there was a ton of existing code that you would import under the name you wanted, the native proposal needed to support that too. That's what export default is for. And I'm totally against it.
When you have a default export in JavaScript you can import it however you'd like.
// user-repository.js
export default class UserRepository {}
// somewhere else
import UserRepository from './user-repository'
import UsersRepo from './user-repository'
import Whatever from './user-repository'If I cared enough to give something a meaningful name, why can you import using another name?
What's worse is that if I decide to rename the symbol using the IDE refactor, it usually doesn't pick every place to rename it (independently of the IDE). This happens because with a default export there is no shared symbol name, only a file path, so the IDE has nothing to match between the export and the import.
And it keeps going! If you use default exports to export a whole object you are actually making your larger since it breaks .
So instead of this:
// utils.js
export default {
formatDate,
formatCurrency,
formatPercentage,
}
// somewhere else
import utils from './utils'
utils.formatDate(new Date())Do this:
// utils.js
export function formatDate() {}
export function formatCurrency() {}
export function formatPercentage() {}
// somewhere else
import { formatDate } from './utils'
formatDate(new Date())What about imports that don't know the name beforehand, like when I'm doing lazy loading for routes?
const UserPage = React.lazy(() => import('./user-page'))Ok, that's like the few places where I actually think it is warranted. I still create a named export and a file that imports the named export and default exports it
// user-page.lazy.js
export { UserPage as default } from './user-page'#Kebab-case vs camelCase (or PascalCase) file names
In the React world it was established as a convention that components would follow the PascalCase convention:
// UserProfileCard.jsx
export function UserProfileCard({ user }) {}This would mirror how a component is actually used in the code:
<UserProfileCard user={user} />A case for cases
camelCase userProfileCard
PascalCase UserProfileCard
snake_case user_profile_card
kebab-case user-profile-cardSince new projects were veering away from jQuery and towards React it just looks weird to have a project where React components were PascalCase and other files were camelCase. At least that's what I think happened.
However, in projects with camelCase and PascalCase you can run into different problems.
The first one to me is readability.
Spot the error:
Spot the error
One of these file names has a typo. Click it.
Also consistency:
src/parsers/HTMLParser.ts
src/parsers/HtmlParser.ts
src/clients/HTTPClient.ts
src/clients/HttpClient.tsMoreover, you can potentially have problems renaming files in case-insensitive file systems like macOS or Windows (use git mv!).
So that's why I would always choose kebab case names.
src/core/components/tooltip-text/tooltip-text.tsx
src/core/metadata/generate-page-metadata.ts
src/features/talks/domain/talk-locations.ts#Named constructor parameters
When you have a class in JavaScript you can have a constructor:
class Temperature {
constructor(celsius) {
this.celsius = celsius
}
}A constructor allows you to create an instance of a class:
const temperature = new Temperature(20)And we can pass through the constructor parameters so we can configure the way that class is created
const freezing = new Temperature(0)
const boiling = new Temperature(100)But what if you want to have some sort of named constructor? For example, to create a Temperature from Fahrenheit.
Perhaps you can create a unit parameter and then have an if statement in the constructor:
class Temperature {
constructor(value, unit) {
if (unit === 'fahrenheit') {
this.celsius = ((value - 32) * 5) / 9
} else {
this.celsius = value
}
}
}
const temperature = new Temperature(68, 'fahrenheit')Or if you jump to TypeScript you can have constructor overload
class Temperature {
constructor(celsius: number)
constructor(value: number, unit: 'celsius' | 'fahrenheit')
constructor(value: number, unit: 'celsius' | 'fahrenheit' = 'celsius') {
this.celsius = unit === 'fahrenheit' ? ((value - 32) * 5) / 9 : value
}
}But if you want to give it a name, there doesn't seem to be a clean solution, is there?
Well, we can have a static method that creates the instance. And that static method can have a name
class Temperature {
constructor(celsius) {
this.celsius = celsius
}
static fromCelsius(celsius) {
return new Temperature(celsius)
}
static fromFahrenheit(fahrenheit) {
return new Temperature(((fahrenheit - 32) * 5) / 9)
}
}
const room = Temperature.fromCelsius(20)
const sameRoom = Temperature.fromFahrenheit(68)Simple yet elegant.
This pattern could fit into the factory design patternOpen in a new tab
#Named parameters
Something that builds on top of the previous pattern is to have named parameters. Let me explain, when you have a function or method that has a lot of parameters:
createUser('César', 'Alberca', 'cesar@cesalberca.com', true, false, 'es')It's quite easy to make a mistake in their position and when you read a it's impossible to know what they refer to if there are multiple ones.
A function with one parameter is called unary, two parameters binary and three ternary. Interesting stuff!
The solution? Objects! Convert the parameters to an object and that helps you with this particular problem.
createUser({
name: 'César',
surname: 'Alberca',
email: 'cesar@cesalberca.com',
isAdmin: true,
hasNewsletter: false,
locale: 'es',
})For my classes in TypeScript I have a constructor using private readonly and then named constructors using the static functions.
type UserRaw = {
name: string
surname: string
email: string
isAdmin: boolean
}
class User {
private constructor(private readonly raw: UserRaw) {}
static create(raw: Omit<UserRaw, 'isAdmin'>): User {
return new User({ ...raw, isAdmin: false })
}
static createAdmin(raw: Omit<UserRaw, 'isAdmin'>): User {
return new User({ ...raw, isAdmin: true })
}
}
const cesar = User.createAdmin({
name: 'César',
surname: 'Alberca',
email: 'cesar@cesalberca.com',
})#Pinned dependencies
I always pin dependencies, and you should too. This little symbol ^ has been part in some way of the biggest security threats in the ecosystem since its inception.
npm dependencies follow semverOpen in a new tab. When you install using npm (and other major package managers):
npm install reactIt by default registers the dependency in your package.json with that caret.
{
"dependencies": {
"react": "^19.2.0"
}
}And surprise surprise, even though you might be seeing version ^19.2.0 in your package.json, in reality you could have installed version 19.2.14 or 19.3.0. That caret means that it only respects the major version, not the minor or patch version.
^ is called a caret. Its sibling ~ (the tilde) only lets the patch version move.
What will npm install?
Type a version range and see which published versions it accepts. The highlighted one is what a fresh install picks.
- 19.2.0
- 19.2.1
- 19.2.14
- 19.3.0
- 19.45.0
- 20.0.0
5 versions match. A fresh install without a lockfile picks 19.45.0.
What about package-lock.json? Well, they introduced that to store the exact version of every dependency (and the dependencies of the dependencies). It helps, as long as nobody deletes it "to fix a weird error" or changes a range in package.json.
Basically when you run npm install without a lockfile (or with an outdated one) it would automatically update the dependency to the latest version that matches the caret. In turn, this means that your dependencies could differ from your teammates or even from continuous integration!
People tend to do this to free themselves from the hassle of updating the dependencies without considering that if a dependency gets hacked like this caseOpen in a new tab, this otherOpen in a new tab or this otherOpen in a new tab the next job or teammate that runs npm install could expose the system to the attacker.
npm install --save-exact reactUse npm ci in continuous integration. It installs exactly what the lockfile says and fails if package.json and the lockfile disagree, instead of silently resolving new versions.
#Mise
I have a tool to manage the versions of my tools! Different projects sometimes require different versions of Node or Python. To avoid going crazy switching versions I use miseOpen in a new tab to manage that. You can also specify a file in your project so other teammates and CI use the specific versions of that tool:
# mise.toml
[tools]
node = "24.1.0"
python = "3.13"
java = "21"Versions can be exact (24.1.0), fuzzy (24), lts or latest. When you cd into the project mise activates those versions automatically.
#Environment variables
Environment variables are quite useful. Actually, they are very useful. It's a pity they are being exposed by AI agents all the time! Well, no more! I use 1Password and the EnvironmentsOpen in a new tab feature, which allows me to safely store those variables. When something needs to access them I can give access or not and they can't read them directly.
# .env
RESEND_API_KEY=op://Development/cesalberca-web/RESEND_API_KEYop run --env-file=.env -- npm start#Multiple vitest configs
Unit tests and integration tests have different needs, so I split them into different configs. A root config that declares the projects:
// vitest.config.mts
export default defineConfig({
test: {
projects: ['./vitest.config.unit.ts', './vitest.config.it.ts'],
},
})One for unit tests, that run in Node:
// vitest.config.unit.ts
export default defineConfig({
test: {
name: 'unit',
include: ['**/*.test.unit.ts'],
environment: 'node',
},
})And one for integration tests, that run in a real browser:
// vitest.config.it.ts
export default defineConfig({
test: {
name: 'integration',
include: ['**/*.test.it.tsx'],
browser: {
enabled: true,
provider: playwright(),
instances: [{ browser: 'chromium' }],
},
},
})This way I can run vitest --project unit on every save and keep the slow ones for later.
#Pre-commit and pre-push
I use lefthookOpen in a new tab to run checks before the code leaves my machine. On pre-commit I only lint and format the staged files:
# lefthook.yml
pre-commit:
commands:
check:
glob: '*.{js,ts,cjs,mjs,jsx,tsx,json,jsonc}'
run: npx @biomejs/biome check --write {staged_files}
stage_fixed: trueAnd on pre-push the slower checks:
pre-push:
commands:
types:
run: npm run type:check
tests:
run: npm run test:unit#.editorconfig
The humblest file in the repository. It tells every editor (and every AI agent) how to write files: indentation, line endings and trailing whitespace.
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
[*.md]
max_line_length = off
trim_trailing_whitespace = false#Conclusion
And that would be some of my top tips for Frontend Development, I hope you enjoyed it! If you don't want to miss out on new posts subscribe to the newsletter hereOpen in a new tab.


