We use all of these words to refer to software —code bases and, if written in compiled languages, their corresponding executable binaries.
We intuitively understand the differences between each of them, but I find there isn't a clear definition that separates one from the other, which leads to gray areas in between.
Over the years as a backend, frontend and fullstack developer, I came up with the following definitions, which helps me communicate more clearly.
These are somewhat personal definitions, but I think they make sense and are practical:
Applications
An application is a code package that can "run" — it has to be started and stopped.
For example, all backend servers are applications. The process stays alive until something or someone kills it: a crash, Kubernetes, CI/CD, CTRL+C, etc.
Things like Photoshop and OBS also fall in this category, even though they are completely different from what we usually do as backend and frontend developers.
Applications can export things, but they shouldn't.
Libraries and Frameworks
These two are closely related, more so than any of the other, so I'll group them to facilitate highlighting their similarities and differences.
Libraries
A library can't run on its own, it just exports stuff that you import from other packages.
They must also be stateless and have no side effects upon import. Nothing prevents a library from being stateful, or causing side effects when imported, but it is bad practice for one to do so, since it leads to hard-to-debug errors in edge cases.
You can have many instances of a library living in parallel and nothing bad will happen.
Libraries, unlike frameworks, neither force nor guide the architecture of your application.
Examples of libraries are lodash
, ramda
and luxon
.
Frameworks
A framework is like a library, with at least one of the following differences:
- It is stateful.
- It largely guides, or even imposes, a specific software architecture upon the application that uses it.
Having more than one of these at run time is guaranteed to blow up in odd and unexpected ways.
Some people will also take another rule into account: it controls or affects the life cycle of the process at run-time. I'll discuss this case in more detail in the next section.
ReactJS, for example, has a DI container you access via setState
and other hooks. This DI container is practically invisible to the developer, and not normally referred to as a DI container, but it's there. It also has the virtual DOM.
React checks that only once instance is loaded and prints a warning to the console otherwise, but most frameworks aren't as kind.
NestJS is another example of this: it internally keeps a graph of all modules, services and how they use each other; and a DI container. Having more than once instance of NestJS at runtime is sure to cause problems.
Intuitively, using a framework feels like it's in control and we're just providing it chunks of code. We may control the entry-point, such as React's createRoot
and Nest's NestFactory.create
, but after that everything is pretty much automagical.
Library vs Framework: Philosophy
Following these rules, ReactJS could be considered a framework and ExpressJS a library, even though they both state the exact opposite.
From the ExpressJS home page:
Express is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications.
And, from the ReactJS home page:
A JavaScript library for building user interfaces
So, what's going on?
These two projects, as many others, are declaring their intent and philosophy. Even though they may not strictly be a library or framework, they want to be considered that.
ReactJS
ReactJS, for example, always tried as hard as possible to be a library. There's no "React" way to build frontends — most React applications will look very different from one another, other than the fact that they use React components.
The React team won't tell you how to organize your folders, whether to place each component in a different file or to have all of them in a single file, how you should manage your state, how big or small a component should be, what the best approach to styling components is, how to bundle your entire application into one or a few files, how to handle routing, etc. They even stopped maintaining react-shallow-renderer, which was used for testing, completely leaving all solutions to testing to the community.
They believe any one solution they come up with won't be as good as what the community can come up with.
In a way, they follow the unix philosophy of "do one thing and do it well".
This degree of freedom led to an amazing ecosystem of architectural patterns, libraries and frameworks built on top of ReactJS, but it also comes with a cost: a bit of added complexity.
NextJS
NextJS is a framework built on top of React which greatly simplifies a bunch of features, the main one being server-side rendering, which is very challenging to tackle without a framework.
It also imposes an architecture by default. For example pages are files inside the pages
folder. It comes with WebPack as a bundler, hosts the server for you out of the box with next start
and even auto-detects, do a degree, whether a page needs to be built statically or dynamically.
What you lose in freedom you gain in features and development speed, and vice versa.
ExpressJS
ExpressJS, on the other hand, doesn't really force you to do anything in any particular way, and largely provides one very specific feature: listening to HTTP requests and responding to them.
In practice, due to its position in the JavaScript universe and its historical significance, developers using ExpressJS tended to follow the same architectural patterns.
The ExpressJS docs even have examples of how to connect to popular databases, how and why to use process managers, or how to gracefully shut down using process.on('SIGTERM', ...
, even though those examples are completely unrelated to ExpressJS itself.
They also provide an express application generator, which does create all the scaffolding, including templating and css solutions.
There's also a second possible reason for it to be considered a framework: to a degree, it controls the life cycle of the run-time process. It won't start it or finish it, nor does it hook into signals, but it does cause it to stay alive until you call server.close()
.
In my opinion, this last rule isn't strong enough to consider a package a framework rather than a library.
I suspect this is why they state NextJS is a minimal framework.
Scripts
A script is a code package that can be run, but runs once and finishes in one go — it isn't long-lived; the process starts, does stuff and exists automatically.
User interaction, if any, happens only through command-line arguments, stdin/stdout and environment variables.
Technically, scripts are applications too, in the sense that they become an OS process, but in practice they solve different problems; and we don't intuitively think of them as applications, in the same way we think of Photoshop or an Express server as applications.
Cron Jobs
A cron job is just a script that is scheduled to run automatically and periodically, rather than being executed manually.
Unlike scripts, cron jobs don't take arguments from stdin, since cron
can't provide them — the script would just block.
By the way, we say cron job as a synonym of job probably because UNIX cron is the most famous application used to schedule jobs, but they aren't necessarily run by UNIX cron
. See Kubernetes CronJob, for example.
There are also more complex job scenarios, such as distributed job execution, automatic retrial, concurrency, etc. Those covered by powerful tools such as BullMQ.
Conclusion
I believe having clear naming rules and boundary definitions helps set expectations. Developers intuitively expect certain behaviours from code packages — specially if they are third-party.
These are definitions that I came up with over the years as a backend, frontend and fullstack developer. They have helped me communicate more clearly with my teams. I hope they'll help others, too.
Acknowledgments
Huge thanks to Wally and Jero for an early review of this article and their valuable input.