A Comprehensive Introduction to Webpack, the Module Bundler

As a front-end Javascript developer, managing your source files can quickly get tricky. On the back-end side, Node has a built-in module resolver —require— whereas there is no built-in module resolver in the browsers.

In this article, I will get you started with Webpack, a very powerful tool to organize, compile and package your source files. Webpack is an module bundler that will let you compile, optimize and minify all your files: Javascript, CSS, pictures, etc.

You might have heard about Gulp as another module bundler. Compared to Webpack, Gulp can be seen as a scripting platform, that will let you chain different building tasks. Webpack is an all-in-one solution, capable of handling all your source files. Moreover, Webpack offers some very powerful features that will make your developments faster:

  • a module resolver for your front end source code
  • a hot reloader, that will replace any modified part of your code directly in the browser when you save the source file, without any page refresh.

Instead of presenting a Webpack boilerplate or starter kit, I will show you how to build a web application step by step from scratch, so that you will end up with a fully working solution that you are comfortable coding with.

Let’s get started!

Basic project structure

First of all, we need to organize our project’s folders. Here is a common directory structure that has proven its worth for many people:

.
├── src                           // The main source folder, where all our source files will go (Webpack's input)
|   └── your-first-js-file.js
├── dist                          // Built files folder (Webpack's output)
├── package.json
└── webpack.config.js             // Webpack config file

We will go into more detail regarding each file later in the article.

Package installation

In order to use Webpack, we first need to fetch the corresponding module. We’ll assume you’re running Node v4+.

All commands will be run from your project’s root folder. If you don’t have a package.json in your project already, then run npm init.

Now let’s install Webpack:

npm install --save webpack

Minimum configuration

Now that we have Webpack installed, let’s configure it. This is done with a plain JS file, webpack.config.js.

The bare minimum to make Webpack compile our sources is to provide it with an entry point as well as an output.

var config = {
    entry: './src',               // entry point
    output: {                     // output folder
        path: './dist',           // folder path
        filename: 'my-app.js'     // file name
    }
}
module.exports = config;

We will now add an npm script to the package.json to call Webpack compilation:

// package.json

// ...

"scripts": {
    "build": "webpack"
}

// ...

Now, when running npm run build, Webpack will look the webpack.config.js up and compile our application.

As you set in the config, our application’s entry point is ./src. We therefore need to define this entry point:

// src/index.js
console.log('Hello Webpack!');

Now that we have our entry point, let’s build the application!

npm run build

As a result, Webpack will bundle up our src/index.js into dist/my-app.js. If you look at the produced file, you’ll notice that Webpack has added some code of its own. That is the module resolver. This provides a require function, exactly as in a NodeJS script!

Add an index.html to our project, so that we can see the effects of the JS files.

<!-- index.html -->
<html>
    <head>
        <script type="text/javascript" src="./dist/my-app.js"></script>
    </head>
    <body>
    </body>
</html>

Manually open this file with your favorite web browser, and see the Hello Webpack! in the console.

Modularity

Let’s recap what we have done so far. We have configured Webpack to bundle the entry point –src/index.js– into an output bundle –dist/my-app.js-. We then created an index.html that calls the bundle. This way, all the code inside src/index.js is executed in the browser when we manually open the index in a browser.

The next step is to separate our code into multiple files.

To build our source files, Webpack will start by compiling the entry point provided in the configuration file. It will then move from require to require (or import in ES6), and include every ’required’ file in the build pipe.

Let’s see how to include other files to the bundled output:

// src/greet.js
function greet(who) {
    console.log('Hello ' + who + '!');
};

module.exports = greet;       // Exposes the greet function to a require in another file
// src/index.js
var greet = require('./greet');   // Import the greet function

greet('Webpack');

As you see, we can require local files by providing a relative path. Notice that we don’t need to specify the file extension, since it’s a JS file. Webpack can manage a bunch of default extensions to know which file it will look for.

Another important thing to notice is that you might eventually end up with very long relative paths when your application grows. For example, it is quite common to depend on a file located in a different folder, and you will find yourself writing imports such as require('../../../../components/home/dashboard/title').

To avoid such messy paths, it is possible to tell Webpack which folder it can consider as the application’s root folder. In our case, src is the root folder:

// webpack.config.js

var path = require('path');
var SRC = path.join(__dirname, 'src/');
var NODE_MODULES = path.join(__dirname, 'node_modules/');

// ...
  resolve: {
    root: [SRC, NODE_MODULES],                  // root folders for Webpack resolving, so we can now call require('greet')
    alias: {
      'actions': path.join(SRC, 'actions/'),    // sample alias, calling require('actions/file') will resolve to ./src/actions/file.js
      // ...
    }   
  },
// ...

With the ability to use modules, we now have very clean code organization. This is because it has been split into several files and folders, depending on what your application is.

Moreover, we can manage our dependencies just as we would do in a Node application. Therefore, we can import external libraries by calling require('library-name').

This will ask Webpack to resolve the module library-name, by looking into the node_modules folder (Webpack will look into other default folders too, see there).

Let’s modify our greet function to use an external library:

// src/greet.js
var moment = require('moment'); // Add momentjs

function greet(who) {
    console.log('Hello ' + who + ', it\'s ' + moment().format('h:mm:ss a') + '!');
};

module.exports = greet;

Compilation

So far we have our source files bundled up into a single output file, which is read by the index.html.

That’s a good starting point for building our web application, but unless we want to write it in plain old ES5 Javascript, we need to actually transform the files while they are bundled together.

Let’s say for instance we want to use the new Javascript specifications, EcmaScript2015 and EcmaScript2016.

Since we are building an application for the web, we need to maximize the browser compatibility of our code. Therefore, we need to transpile any ES6/ES7 code to ES5, which is supported by all modern browsers (do not hesitate to use the excellent caniuse website to check any browser-related compatibility questions).

We will use Babel to transpile ES6 and ES7.

Let’s install Babel and its presets for ES6 and ES7:

npm install --save-dev babel babel-preset-es2015 babel-preset-stage-0

We need to configure it to use these presets:

// package.json

// ...
    "babel": {
        "presets": [
            "es2015",      // ES6 compilation ability
            "stage-0"      // ES7 compilation ability
        ]
    },
// ...

Babel is now configured to transpile JS files, but it’s still not working together with Webpack.

Let me introduce the concept of Webpack loaders. Loaders are used by Webpack in order to handle a given file type.

Everytime Webpack reads a require() or an import, it will handle the content of the required file with a loader.

By default, Webpack comes with a built in JS handler, which tells it how to handle plain JS files and bundle them up. In order to handle different file types, or simply deal with JS files in a different manner, we must specify a loader configuration to Webpack.

As you might have guessed, the link between Webpack and Babel will be made by a loader.
It’s called… babel-loader!

First we need to download it:

npm install --save-dev babel-loader

Now that we have it as a dependency, we must tell Webpack where to use it.

// webpack.config.js
var config = {
    entry: './src',
    output: {
        path: './dist',
        filename: 'my-app.js'
    },
    module: {
      loaders: [
        {
          test: /\.js$/,
          loaders: ['babel']      // note that specifying 'babel' or 'babel-loader' is equivalent for Webpack
        }
      ]
    }
}
module.exports = config;

This will add a new loader to Webpack.

Every time Webpack sees a require('xxx.yy'), it will loop through all its configured loaders and check if xxx.yy matches the provided test regexp (in our case, /\.js$/).

As you can infer now, the babel-loader will be used to compile every .js file. We can therefore rewrite our good old ES5 files into ES6/ES7 files!

Managing other file types

Now that we are clear on the loader concept, we can manage other file types. In this article, I’ll show you how to handle some common file types, such as fonts and CSS files; however, keep in mind that there are a LOT of loaders on npmjs for almost any file type.

Style files

Plain CSS files

There are two loaders for .css files, called style-loader and css-loader.

You can try to figure out by yourself how to add these loaders in order to handle .css files.

Here is a suggestion of configuration:

// webpack.config.js

// ...
  {
    test: /\.css$/,
    loaders: ['style', 'css'] // Note that the order is important here, it means that 'style-loader' will be applied to the ouput of 'css-loader'
  },
// ...

NOTE: the CSS will be added to the bundled file; in our case, my-app.js. The style will be loaded in your browser, but you might be used to having separate .css files. If you want Webpack to output separate style files, please use ExtractTextWebpackPlugin.

SASS/Less/PostCSS

In case you are using SASS, Less or PostCSS, simply add the corresponding loader:

Font files

If you use specific fonts in your project, Webpack can handle them too!

Since the fonts will be downloaded as such by your client application, use the file-loader:

// webpack.config.js

// ...
    {
      test: /\.(eot|svg|ttf|woff|woff2)$/,
      loader: 'file?name=public/fonts/[name].[ext]'
    }
// ...

This will output all font files to the public/fonts folder.

Images

Similarly to fonts, images are usually served directly by web servers and loaded on demand by the client. The loader configuration is very similar:

// webpack.config.js

// ...
    {
      test: /\.(jpg|png|svg)$/,
      loader: 'file?name=public/images/[name].[ext]'
    }
// ...

Other files

We could continue the list of all possible files Webpack loaders can handle, but this is not the aim here. If you want to load other file types, search for ‘yourFiletype loader’ on the Internet, and you will find the corresponding loader. For example, load .jade files with the jade loader, .cs files with the coffee loader, etc.

Going further

These are the basic usages of Webpack. You might now want to move to more advanced topics about Webpack.

Here is a non exhaustive list of subject you might want to read about:

In the meantime, do not hesitate to read the official documentation to discover new features and grab a deep understanding of Webpack’s multiple options.

Update 05/02/17

I had initially planned to write this article in two parts, putting the more advanced topics in the second parts.
Unfortunately, I could not make it to write the second part, so I added two links to great articles dealing with these topics.


You liked this article? You'd probably be a good match for our ever-growing tech team at Theodo.

Join Us

  • Scott Rothman

    When you updated webpack.config.js to include the directories for node_modules and the like, it’s not easy to tell the structure of the file after your update. Do you define resolve before or after you have exposed the config variable?

  • bernardstanislas

    Yes as you said it’s a key of the config object. The indentation is incorrect, this might have misled you. I’m correcting it right now, thank you !

  • kaseymccurdy

    finally getting around to diving into Webpack and so glad i found this very well written article — really demystified a lot of what webpack is and how to use it. thanks for getting me started…now, off to the official docs!

  • bernardstanislas

    Thank you Kasey for your feedback, hopefully you’ll learn a lot from the official documentation !

  • quazecoatl

    Great job, thanks for taking time to write this.
    Please talk about production configuration vs dev configuration. Also, after you cover these and other stuff you probably have in mind, what do you think about talking a little bit about how stuff actually works in webpack. I mean, talk a bit about internals. How does it parses my code, it some high level stuff, it doesn’t have to go too specific, but still describe how its doing some of the magic.

    Also, how about having a repository on GitHub for people to suggest you ideas? You can tag them or reject them (close them).
    Or, how about turning this into a book? Or it might be too much work, blog is fine

    Thanks!

  • bernardstanislas

    Thanks for your feedback !
    As you asked, I will talk about the different configurations in the second part, along with more advanced Webpack usage.
    Depending on how long the next article will be, I can quickly go over how it works internaly, to give you hints about where to look for if you are interested in understanding the whole tool.
    For the repo it’s a very good idea, but I don’t want to put too much things around the article, I want to keep it as simple as possible.
    If any other suggestion comes to your mind, don’t hesitate to post them here !
    Thank you !

  • Rithi

    Great post! I’m just learning webpack and this is helping a lot. I’m trying to run the modified greet.js file that loads moment.js but I am getting this error in the console, “Uncaught ReferenceError: moment is not defined”.

    I have moment.js loaded in node_modules and references to it in webpack.config.js under ‘externals’ and package.json under ‘devDependecies’. What am I doing wrong?

  • bernardstanislas

    Thanks for your feedback !
    The problem is you souldn’t add any library reference in your webpack.config.js
    If you put moment in the externals section, Webpack will consider that it shouldn’t build the library, considering it as an external library loaded without Webpack.
    To solve your problem, simply remove the externals section in your webpack.config.js :)

  • Good detailed post! I was wondering if there is any specific reason for using `–save` instead of `–save-dev` when you’re installing `webpack` as I consider it a devDependency.

  • bernardstanislas

    Actually, the only difference between –save and –save-dev (ie dependencies vs dev dependencies) is that dev dependencies will not be installed if you publish your project on npmjs.org as a module.
    If someone adds your module as a dependency, it will be installed in the node_modules folder and only the dependencies you have specified will be installed along with the module, skipping the dev dependencies.

  • Exactly, but what’s your reason for using –save instead of –save-dev when installing webpack?

  • bernardstanislas

    Oh for this there is not particular reason, I don’t have an opinion on whether a package should be brought to npmjs.org already compiled, or compile it in a postinstall script. For the later, you might need to leave Webpack as a dependency in order to be able to compile the project once required as a dependency on another project.

  • Anders Ramsay

    By default, you would want use –save-dev. If a need for some postinstall script arises, then you might choose to move it to –save. However. I think you will confuse people new to npm by using –save when, imo, the default choice should be –save-dev

  • Martin Dobrev

    Great article, waiting for part 2!

  • Great article, THS.

  • Franz Josef Kaiser

    How about part 2? Did it ever happen?

  • Stanislas Bernard

    Hi Franz, I’m not currently working on it because I don’t have the time these days. But thank you for the reminder, I will get back to it as soon as possible :)

  • Pierre-Louis Le Portz

    Such a helpful article !
    Late readers be careful: the configuration object was modified in the Webpack 2 release and resolve.root was replaced with resolve.modules

    Thank you Stanislas

  • Nicolas Girault

    Waooooooou

  • VijayaKumar Raja

    Thank you so much 😉