The Anatomy of a Modern JavaScript Application

For a high-quality, in-depth introduction to ES6, you can’t go past Canadian full-stack developer Wes Bos. Try his course here, and use the code SITEPOINT to get 25% off and to help support SitePoint.

There’s no doubt that the JavaScript ecosystem changes fast. Not only are new tools and frameworks introduced and developed at a rapid rate, the language itself has undergone big changes with the introduction of ES2015 (aka ES6). Understandably, many articles have been written complaining about how difficult it is to learn modern JavaScript development these days.

In this article, I’ll introduce you to modern JavaScript. We’ll take a look at the most recent developments in the language and get an overview of the tools and techniques currently used to write front-end web applications. If you’re just starting out with learning the language, or you’ve not touched it for a few years and are wondering what happened to the JavaScript you used to know, this article is for you.

A Note about Node.js

Node.js is a runtime that allows server-side programs to be written in JavaScript. It is possible to have full-stack JavaScript applications, where both the front and back-end of the app is written in the same language. Although this article is focused on client-side development, Node.js still plays an important role.

The arrival of Node.js had a significant impact on the JavaScript ecosystem, introducing the npm package manager and popularizing the CommonJS module format. Developers started to build more innovative tools and develop new approaches to blur the line between the browser, the server, and native applications.

JavaScript ES2015+

In 2015, the sixth version of ECMAScript—the specification that defines the JavaScript language—was released under the name of ES2015 (still often referred to as ES6). This new version included substantial additions to the language making easier and more feasible to build ambitious web applications. But improvements don’t stop with ES2015; each year, a new version is released.

Declaring variables

JavaScript now has two additional ways to declare variables: let and const.

let is the successor to var – although var is still available, let limits the scope of variables to the block (rather than the function) they’re declared within, which reduces the room for error:

Using const allows you to define variables that cannot be rebound to new values. For primitive values such as strings and numbers, this results in something similar to a constant, as you cannot change the value once it has been declared.

Arrow functions

Arrow functions provide a cleaner syntax for declaring anonymous functions (lambdas), dropping the function keyword and the return keyword when the body function only has one expression. This can allow you to write functional style code in a nicer way.

Improved Class syntax

If you are a fan of object-oriented programming, you might like the addition of classes to the language on top of the existent mechanism based on prototypes. While it is just syntactic sugar, it provides a cleaner syntax for developers trying to emulate classical object-orientation with prototypes.

Promises / Async functions

The asynchronous nature of JavaScript has long represented a challenge; any non-trivial application ran the risk of falling into a callback hell when dealing with things like Ajax request.

Fortunately, ES2015 added native support for promises. Promises represent values that don’t exist at the moment of the computation but that may be available later, making the management of asynchronous function calls more manageable without getting into deeply nested callbacks.

ES2017 (due out this year) introduces async functions (sometimes referred to as async/await that make improvements in this area, allowing you to treat asynchronous code as if it were synchronous.

Modules

Another prominent feature added in ES2015 is a native module format, making the definition and usage of modules a part of the language. Loading modules was previously only available in the form of third-party libraries. We’ll look at modules in more depth in the next section.

There are other features we won’t talk about here, but we’ve covered some of the major differences you’re likely to notice when looking at modern JavaScript. You can check a complete list with examples on the Learn ES2015 page on the Babel site, which you might find useful to get up to date with the language. Some of those features include template strings, iterators, generators, new data structures such as Map and Set, and more.

Code linting

Linters are tools that parse your code and compare it against a set of rules, checking for syntax errors, formatting, and good practices. Although the use of a linter is recommended to everyone, it is especially useful if you are getting started. When configured correctly for your code editor/IDE you can get instant feedback to ensure you don’t get stuck with syntax errors as you’re learning new language features.

Modular Code

Modern web applications can have thousands (even hundred of thousands) of lines of code. Working at that size becomes almost impossible without a mechanism to organize everything in smaller components, writing specialized and isolated pieces of code that can be reused as necessary in a controlled way. This is the job of modules.

CommonJS modules

A handful of module formats have emerged over the years, the most popular of which is CommonJS. It’s the default module format in Node.js, and can be used in client-side code with the help of module bundlers, which we’ll talk about shortly.

It makes use of a module object to export functionality from a JavaScript file and a require() function to import that functionality where you need it.

ES2015 modules

ES2015 introduces a way to define and consume components right into the language, which was previously possible only with third-party libraries. You can have separate files with the functionality you want, and export just certain parts to make them available to your application.

Note: Native browser support for ES2015 modules is still under development, so you currently need some additional tools to be able to use them.

Package Management

Other languages have long had their own package repositories and managers to make it easier to find and install third-party libraries and components. Node.js comes with its own package manager and repository, npm. Although there are other package managers available, npm has become the de facto JavaScript package manager and is said to be the largest package registry in the world.

In the npm repository you can find third-party modules that you can easily download and use in your projects with a single npm install <package> command. The packages are downloaded into a local node_modules directory, which contains all the packages and their dependencies.

The packages that you download can be registered as dependencies of your project in a package.json file, along with information about your project or module (which can itself be published as a package on npm).

You can define separate dependencies for both development and production. While the production dependencies are needed for the package to work, the development dependencies are only necessary for the developers of the package.

Build Tools

The code that we write when developing modern web applications almost never is the same code that will go to production. We write code in a modern version of JavaScript that may not be supported by the browser, we make heavy use of third-party packages that are in a node_modules folder along with their own dependencies, we can have processes like static analysis tools or minifiers, etc. Build tooling exists to help transform all this into something that can be deployed efficiently and that is understood by most web browsers.

Module bundling

When writing clean, reusable code with ES2015/CommonJS modules, we need some way to load these modules (at least until browsers support ES2015 module loading natively). Including a bunch of script tags in your HTML isn’t really a viable option as it would quickly become unwieldy for any serious application, and all those separate HTTP requests would hurt performance.

We can include all the modules where we need them using the import statement from ES2015 (or require, for CommonJS) and use a module bundler to combine everything together into one or more files (bundles). It’s this bundled file that we are going to upload to our server and include in our HTML. It will include all your imported modules and their necessary dependencies.

There are a currently a couple of popular options for this, the most popular ones are Webpack, Browserify and Rollup.js. You can choose one or another depending on your needs.

Transpilation

In order to make our modern JavaScript work, we need to translate the code we write to its equivalent in an earlier version (usually ES5). The standard tool for this task is Babel; a compiler that translates your code into compatible code for most browsers. In this way you don’t have to wait for vendors to implement everything, you can just use all the modern JS features.

There are a couple of features that need more than a syntax translation; Babel includes a Polyfill that emulates some of the machinery required for some complex features, like promises.

Build systems & task runners

Module bundling and transpilation are just two of the build processes that we may need in our projects. Others include code minification (to reduce files sizes), tools for analysis, and perhaps tasks that don’t have anything to do with JavaScript, like image optimizations or CSS/HTML pre-processing.

The management of tasks can become a laborious thing to do, and we need a way to handle it in an automated way, being able to execute everything with simpler commands. The two most popular tools for this are Grunt.js and Gulp.js, they provide a way to organize your tasks into groups in an ordered way.

For example, you can have a command like gulp build which may run a code linter, the transpilation process with Babel, and module bundling with Browserify. Instead of having to remember three commands and their associated arguments in order, we just execute one that will handle the whole process automatically.

Wherever you find yourself manually organizing processing steps for your project, think if it can be automatized with a task runner.

Application Architecture

Web applications have different requirements than websites. For example, while page reloads may be acceptable for a blog, that is certainly not the case for an application like Google Docs. Your application should behave as close as possible to a desktop one, otherwise, the usability would be compromised.

Old-style web applications were usually done by sending multiple pages from a web server, and when a lot of dynamism was needed, content was loaded via Ajax by replacing chunks of HTML according to user actions. Although it was a big step forward to a more dynamic web, it certainly had its complications; sending HTML fragments or even whole pages on each user action represented a waste of resources, especially time from the user’s perspective. The usability still didn’t match the responsiveness of desktop applications.

Looking to improve things, we created to new methods to build web applications, from the way we present them to the user to the way we communicate the client with the server. Although the amount of JavaScript required for an application also increased drastically, the result is now applications that behave very closely to native ones; without page reloading or extensive waiting periods each time we click a button.

Single Page Applications (SPAs)

The most common high-level architecture for web applications is called SPA, which stands for Single Page Application. SPAs are big blobs of JavaScript that contain everything the application needs to work properly. The UI is rendered entirely client-side, so no reloading is required. The only thing that changes is the data inside the application, which is usually handled with a remote API via Ajax or another asynchronous method of communication.

One downside to this approach is that the application takes longer to load for the first time. Once it has been loaded, however, transitions between views (pages) are generally a lot quicker, since it is only pure data being sent between client and server.

Universal / Isomorphic Applications

Although SPAs provide a great user experience, depending on your needs, they might not be the optimal solution. Especially if you need quicker initial response times or optimal indexing by search engines.

There is a fairly recent approach to solving these problems called Isomorphic (or Universal) JavaScript applications. In this type of architecture, most of the code can be executed both on the server and the client. You can choose what you want to render on the server for a faster initial page load, and after that, the client takes over the rendering while the user is interacting with the app. Because pages are initially rendered on the server, search engines can index them properly.

Deployment

With modern JavaScript applications, the code that you write is not the same as the code that you deploy for production; you only deploy the result of your build process. The workflow to accomplish this can vary depending on the size of your project, the number of developers working on it, and sometimes the tools/libraries that you are using.

For example, if you are working alone on a simple project, each time you are ready for deployment you can just run the build process and upload the resulting files to a web server. Keep in mind that you only need to upload the resulting files from the build process (transpilation, module bundling, minification, etc.), which can be just one .js file containing your entire application and dependencies.

Having all of your application files in an src directory, written in ES2015, and importing packages installed with npm and your own modules from a lib directory.

Then you can run Gulp, which will execute the instructions from a gulpfile.jsto build your project: bundling all modules into one file (including the ones installed with npm), transpiling ES2015+ to ES5, minifying the resulted file, etc. Then you can configure it to output the result in a convenient dist directory.

Note: If you have files that don’t need any processing, you can just copy them from src to the dist directory. You can configure a task for that in your build system.

Now you can just upload the files from the dist directory to a web server, without having to worry about the rest of the files, which are only useful for development.

Team development

If you are working with other developers, it is likely that you are also using a shared code repository, like GitHub, to store the project. In this case, you can run the building process right before making commits and store the result with the other files in the Git repository, to later be downloaded onto a production server.

However, storing built files into the repository is prone to errors if several developers are working together and you might want to keep everything clean from build artifacts. Fortunately, there is a better way to deal with that problem: you can put a service like Jenkins, Travis CI, CircleCI, etc. in the middle of the process, so it can automatically build your project after each commit is pushed to the repository. Developers only have to worry about pushing code changes without building the project first each time, also the repository is kept clean of automatically generated files, and at the end, you still have the built files available for deployment.

Conclusion

The transition from simple web pages to modern JavaScript applications can seem daunting if you have been away from web development during recent years, but I hope this article was useful as a starting point. I’ve linked to more in-depth articles on each topic where possible, so you can explore further.

And remember that if at some point, after looking all the options available, everything seems overwhelming and messy; just keep in mind the KISS principle, and use only what you think you need and not everything you have available. At the end of the day, solving problems is what matters, not using the latest of everything.

What is your experience of learning modern JavaScript development? Is there anything I haven’t touched on here that you’d like to see covered in future? I’d love to hear from you in the comments!