Getting started with Grunt

Kevin Lorenz (@verpixelt)
16 min readMar 4, 2015

In this article I would like to explain what exactly Grunt is, what it can do for you and how you get it up and running. During this article we will walk through all the steps required in order to set up a basic Grunt front end project, featuring: Creating the project structure, compiling Sass to CSS, setting up a local server, live reloading and minifying JavaScript.

So, let’s get started!

Note: This whole article is written from the perspective of a Mac user. I won’t dig into other operating systems. If you have any questions regarding the functionality on other operating systems, please use Google or ask someone to help you.

If you just want to have a quick start you can follow the first two sections of this article (What is Grunt? & Getting things in place), then check out my Grunt boilerplate, install the dependencies with ‘npm install’ and configure your file paths.

What is Grunt?

First of all, what is Grunt? Grunt is a JavaScript based task runner — to be more precise a Node.js based command line tool. It helps you automate tasks you want to run on a regular basis but do not want to do manually over and over again. For example: You’re working on a small website for which you need to run a local server, your preprocessed CSS files compiled to CSS, you need something to minify and concatenate your Javascript files. You maybe want to switch between development and production files for testing purposes, too.

This is something I would call my bare minimum for starting a project. We could go further and add auto reload, image and SVG optimisations, copy tasks and so on. You will find a ton of Grunt plugins on GitHub. It’s like the phrase “There is an app for that” — There might also be a Grunt task for that.

So there are a lot of things a front end developer has to keep in mind these days and Grunt helps you run these tasks over and over again, automatically. Sounds interesting?

Then, shall we start?

Getting things in place

To get started with Grunt we need a few things first. As I already mentioned Grunt is a Node.js based task runner, which means we obviously need Node.js on our system. To get Node.js simply go to http://nodejs.org/ and either click the install button, which leads you to a download of a .pkg file or click on Downloads and chose between one of the given options.

After we have successfully installed Node.js on our system we want to install Grunt, too. So Grunt plugins and also Grunt by itself are managed through npm, the Node.js package manager.

For using npm we have to open our ‘Terminal’. The Terminal can be a bit intimidating at first, but don’t be afraid! We will go through it step by step.

So, Mac OS X has a built in Terminal which is totally fine to use. You will find it by opening the Finder /Applications/Utilities/Terminal.app or by simply typing Terminal into Spotlight. Personally I prefer using ‘iTerm 2’ which is, quoting the official page “… a terminal emulator for Mac OS X which does amazing things.” It is completely free and you can get it right here http://iterm2.com/. (Take this as a hint for future improvements, the Terminal works completely fine for what we’re going to do.)

iTerm 2 on the left, OS X built-in Terminal on the right

So with either the Terminal app or iTerm 2 open we will put in our first command:

npm install -g grunt-cli

This will install Grunt’s command line interface globally (-g), allowing it to be run from any directory. You probably have to use sudo to run this command as an administrator (you’ll get asked for your password), which would look something like this:

sudo npm install -g grunt-cli

After the installation is complete we can check if everything went fine, with the following command:

grunt --version

We should get an output looking like this (grunt-cli v0.1.13).

So what have we achieved by now? We’ve installed Grunt CLI which doesn’t do anything on its own. Its job is to load and run the Grunt installation we place into each of our projects. Which directly leads us to our next step.

Creating the project structure

Next, we have to set up a simple project structure. I like to do these things right in the Terminal. You can follow my instructions, use the Finder or any other way you may prefer.

So all my projects are located inside a folder called ‘projects’ which is inside my home directory. So I navigate there with the following command (cd = change directory):

cd projects

Inside this folder I create a new project folder, called it ‘my-first-grunt-workflow’ and cd in afterwards.

mkdir my-first-grunt-workflow
cd my-first-grunt-workflow

Inside our project folder we have to do a few things. We have to create our first gruntfile.js, a package.json and the rest of our project structure like an index.html, an assets folder and so on. The gruntfile.js and the package.json have to be there. The rest of your project structure of course may differ to mine (but maybe you want to follow just for the purpose of this article).

Remember, ‘mkdir’ creates a folder, ‘touch’ creates a file. The two dots after the cd command just let us jump back one level. Like shown in the image: I jumped back from /my-first-grunt-workflow/assets to /my-first-grunt-workflow which is our project root folder.

This is what my project structure looks like right now.

Javascript and Sass files are empty at this point. We will work with them later.

We have an assets folder which includes a dev (development) and prod (production) folder. Inside the dev folder we have a scss folder for our Sass files, a js folder for our javascript files and a css folder for, yeah you’ve guessed it, our CSS file(s). Don’t be confused about other empty folders. We will get to this later on.

Install Grunt

Now we finally come to the interesting bits of this article. We’re going to install Grunt. For this we have to make sure that we are inside our project’s root folder (the ‘ls’ command lists the directory content). Inside this folder we execute the following command:

npm init

Now we get asked a couple of questions. In terms of this article you can confirm all of them by hitting enter, and complete it by typing in yes at the end. What this command did is, it simply gave our package.json the necessary structure to allow us to install npm plugins.

Hint: npm init would also have created the package.json file which we did earlier by hand.

If you open your package.json this is how it should look right now.

{
"name": "my-first-grunt-workflow",
"version": "1.0.0",
"description": "",
"main": "gruntfile.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}

Now we’re able to install our Grunt plugins. Let’s start with Grunt. Simply type the following command:

npm install grunt --save-dev

—save-dev puts the freshly installed Grunt plugin into the devDependencies section inside our package.json, which tells every developer which plugins are needed for development (and maybe not for a production version).

After we’ve installed our first plugin, the package.json looks a bit different than before.

{
"name": "my-first-grunt-workflow",
"version": "1.0.0",
"description": "",
"main”: "gruntfile.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"grunt": "^0.4.5"
}
}

If you get an error like this ‘Attempt to unlock /Users/verpixelt/projects/my-first-grunt-workflow/node_modules/grunt, which hasn’t been locked’ don’t panic. There’s something wrong with your folder permissions. Without digging deeper into why this is happening here’s a StackOverflow thread with an answer which solves the problem very quickly: NPM cannot install dependencies — Attempt to unlock something which hasn’t been locked.

Now that we have installed Grunt locally inside of our project, we can go through our list of plugins we need, install them via npm and configure our first Grunt task. So let’s see, we want a local server which runs our project.

Grunt contrib connect

For our local server we’re going to use grunt-contrib-connect (contrib means that there are people, of the Grunt core team, who actively contribute to this plugin. Which means existing bugs get solved, new features get build in, etc.).

Like we did with Grunt before we now use the following command to install this plugin:

npm install grunt-contrib-connect --save-dev

If we check our package.json we now have an additional line inside the DevDependencies.

{
"name": "my-first-grunt-workflow",
"version": "1.0.0",
"description": "",
"main": "gruntfile.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"grunt": "^0.4.5",
"grunt-contrib-connect": "^0.9.0"
}
}

Let’s open our gruntfile.js and copy the following code into it. This is our bare-bones Grunt file (you can copy the skeleton from the official Grunt website).

module.exports = function(grunt) {  // Project configuration.
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
// Here we’re going to define our tasks }); // Load Grunt plugins
grunt.loadNpmTasks('');
// Default task(s).
grunt.registerTask('default', []);
};

See the part which says ‘Load Grunt plugins’? Let’s add our first, already installed plugin there.

// Load Grunt plugins
grunt.loadNpmTasks('grunt-contrib-connect');

Grunt CLI now knows which plugin to load but that’s it. We have to configure what our plugin should do for us.

Every plugin comes with a few options, which you normally can see in the README file of the Github repository. Usually every README contains an example of how to use the plugin, also in this case: https://github.com/gruntjs/grunt-contrib-connect#usage-examples.

The absolute minimum for this plugins to run, which is totally fine for our use case, is using the defaults.

module.exports = function(grunt) {  // Project configuration.
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
connect: {
uses_defaults: {}
}
});
// Load Grunt plugins
grunt.loadNpmTasks('grunt-contrib-connect');
// Default task(s).
grunt.registerTask('default', []);
};

Now you can type into your Terminal ‘grunt connect’. This starts the plugin, we’ve just configured. And you should end up with this.

One would normally think that we could now got to localhost:8000 and get something like a page. But we end up with an error. The webpage is not available, sad face.

By knowing how Grunt works, this totally makes sense. Grunt starts at the top of the gruntfile, sees the configuration for the connect task, starts it, and after it has successfully run, Grunt stops it. So as long we don’t provide Grunt with an ongoing task there’s nothing to see (we will do that later).
(To simulate what we expected to happen, we can add the option keepalive: true to our grunt-contrib-connect configuration. This will keep our server alive forever. But keep in mind that any task which follows after this configuration won’t run anymore.)

Grunt sass

We’ve created a style.scss file at the beginning which we want to compile to CSS. In order to do so we will use a C/C++ based version of Sass. You could also use the original Ruby based version if you like. I have no dependencies to Ruby in my dev stack and this version of Sass also called LibSass is notably faster as well.

By now I guess you know how to install this Grunt plugin. Note: This is not a contrib plugin. If everything went well, this is how your package.json should look like by now.

{
"name": "my-first-grunt-workflow",
"version": "1.0.0",
"description": "",
"main": "gruntfile.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"grunt": "^0.4.5",
"grunt-contrib-connect": "^0.9.0",
"grunt-sass": "^0.18.0"
}
}

Let’s configure our Sass task.

module.exports = function(grunt) {  // Project configuration.
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
connect: {
uses_defaults: {}
},
sass: {
dev: {
options: { sourceMap: true },
files: { '' : '' }
}
}
});
// Load Grunt plugins
grunt.loadNpmTasks('grunt-contrib-connect');
grunt.loadNpmTasks('grunt-sass');
// Default task(s).
grunt.registerTask('default', []);
};

What happened here so far: We’ve added a line to tell Grunt which plugin to load and we set up a basic configuration for our task. Like you may have noticed, we haven’t yet specified the files to compile.

Let’s have a look at our folder structure again.

So the paths for our Sass and CSS files would look something like this:

Sass: assets/dev/style.scss CSS: assets/dev/css/style.css

Note: For our Sass task we have to define our target first (CSS file) and then our source (Sass file).

module.exports = function(grunt) {  // Project configuration.
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
connect: {
uses_defaults: {}
},
sass: {
dev: {
options: { sourceMap: true },
files: { 'assets/dev/css/style.css' : 'assets/dev/scss/style.scss' }
}
}

});
// Load Grunt plugins
grunt.loadNpmTasks('grunt-contrib-connect');
grunt.loadNpmTasks('grunt-sass');
// Default task(s).
grunt.registerTask('default', []);
};

If we run grunt sass in the Terminal we get the following file structure.

As you can see, we now have a style.css file and a style.css.map (sourcemap) as well. Of course if we look into our CSS file it is empty because our Sass file is empty, too. Let’s change that very quickly.

I still like to start all my projects with normalize from Nicolas Gallagher. So let’s copy & paste the code into a new file, called _normalize.scss inside our /dev/scss directory. Now we simply import this file in our style.scss file (@import ‘normalize’) and let grunt sass run again.

Voila! Now we end up with our normalize(d) code in our final CSS file.

Now we have grunt-sass in place, but what if you want to ship your code? Maybe the folder structure has given you a hint already. We will define a different output for our production ready files.

Grunt-sass has a few more options for us to play with. One that I want to show you right now is ‘outputStyle’. This defines how your CSS gets rendered. The default here is ‘nested’ which is absolutely fine for development, but for production you want your files to be as small as possible.

Development and production

We will now introduce a separation inside our Sass task between development and production and how files should be handled. To do this, we have to adjust our already build Sass task a bit.

We’ve wrapped our existing configuration inside an object called ‘dev’. Now we can copy these settings, place them right after ‘dev’ and call it ‘prod’. Inside the ‘options’ in ‘prod’ we now add ‘outputStyle: compressed’, set ‘sourceMap’ to ‘false’ and our destination file path for our CSS file changes to assets/prod/CSS/style.css. This is our configuration:

module.exports = function(grunt) {  // Project configuration.
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
connect: {
uses_defaults: {}
},
sass: {
dev: {
options: {
sourceMap: true
},
files: { 'assets/dev/css/style.css' : 'assets/dev/scss/style.scss' }
},
prod: {
options: {
sourceMap: false,
outputStyle: 'compressed'
},
files: { 'assets/prod/css/style.css' : 'assets/dev/scss/style.scss' }
}
}
});
// Load Grunt plugins
grunt.loadNpmTasks('grunt-contrib-connect');
grunt.loadNpmTasks('grunt-sass');
// Default task(s).
grunt.registerTask('default', []);
};

Sass is now split into sass:dev and sass:prod which are two different task we can call separately (e.g. ‘grunt sass:dev’). On the other hand, if you run ‘grunt sass’ now, Grunt would execute both tasks from top to bottom.

We’ve accomplished quite something, but we’re not there, yet.

Grunt-contrib-watch

Should we change something inside our Sass files, it would come in handy if our CSS would get updated as well (this is possible with all kind of files. In this example we will use it for our Sass files). There is a plugin for that, called Grunt-contrib-watch. So we install it like every other plugin before. Remember: We want it to be a dev dependency.

After installing it, we have to tell Grunt to load the plugin.

// Load Grunt plugins
grunt.loadNpmTasks('grunt-contrib-connect');
grunt.loadNpmTasks('grunt-sass');
grunt.loadNpmTasks('grunt-contrib-watch');

Now we start to configure our task. We have to tell the watch task which files we want to watch and what task(s) it should start if some of the files changes. Our configuration looks like this:

watch: {
css: {
files: 'assets/dev/scss/**/*.scss',
tasks: [ 'sass:dev' ]
}
}

The *s inside our file path help us to find all relevant Sass files. The two ** tell to look in all folders and subfolders after /scss and the single * means that we look for all files with the file extension .scss no matter what the file is called.

After the file path, we define which task(s) Grunt should run if one of the Sass files changes. In our case, we want Grunt to run our sass:dev task. But wouldn’t it be awesome if we change for example the background-color of our page and the site reloads automatically? Grunt-contrib-watch has us covered.

We have to do two things to get there. First we have to adjust our configuration as shown below.

watch: {
css: {
files: 'assets/dev/scss/**/*.scss',
tasks: [ 'sass:dev' ],
options: { livereload: true }
}
}

Second, we need something which actually does the reload magic. The GitHub page of the plugin showns different options of how to achieve that. I like the simple way of referencing the livereload script, which we have downloaded by installing the plugin, inside our HTML file(s) (if you’re working on a bigger project it might be better to choose another method or make this work through grunt-contrib-connect).

Now we open our index.html and put the following line before the closing body tag.

<script src="//localhost:35729/livereload.js"></script>

That’s it! You can test it by running the following command inside your Terminal:

grunt connect watch

This starts your local server and the watch task, which looks for changes in the Sass files. If you now, for example, change the background color of your body element, the browser will automatically reload after you’ve saved the document.

Grunt-contrib-uglify

Our last plugin we’re going to add today is grunt-contrib-uglify. This plugin minifies files by using UglifyJS. As we did for the other plugins before we will implement a very basic version of it. You can extend this on your own if you want to go further.

So after installing and referencing the plugin in our gruntfile.js we’re going to configure it. In this case it’s really simple. I’ve downloaded the unminified version of jQuery and put it into our assets/dev/js folder, just to have something we can minify and see if our task is working.

uglify: {
my_targets: {
files: { 'assets/prod/js/output.min.js' : ['assets/dev/js/**/*.js'] }
}
}

Remember from our Sass task, we have to reference our output folder/file first and then our source files. Like we did with the Sass files we also user wild card selectors for our JS files.

After running ‘grunt uglify’ this is what our output.min.js file should look like.

Register Tasks

It is impractical to write commands like ‘grunt connect watch …’ again and again. Especially if you have, let’s say 5–10 tasks, wich should run every time. Luckily we can register tasks, give them a names and call them on that. So how do we gonna do that? Maybe you’ve already noticed this part and the end of your Grunt file:

// Default task(s).
grunt.registerTask('default', []);

In this case, ‘default’ is the name of our task and inside the square brackets we’re going to list all the tasks we want to run. Now let’s assume that our dev scenario is our default. We need our local server aka. grunt connect and our Sass task which is part of our watch task. This leads to the following code:

// Default task(s).
grunt.registerTask('default', [
'connect',
'watch'
]);

Now, let’s run them. Just type grunt into your Terminal. This is what your Terminal output should look right now. If so, we did everything just right =)

Do you remember that we’ve split up our folders and tasks into dev and prod? Let’s make some use of that and implement a production task.

What do we need if we want to ship our code? We don’t need our local server to run, so screw that. We need to run Sass, but we want our code to be neat and tidy and we want our JS files minified.

So this is our final code:

grunt.registerTask('lets-ship-it', [
'sass:prod',
'uglify'
]);

Let’s stop our still running dev task by hitting ‘ctrl + c’ and type ‘grunt lets-ship-it’.

And this is what you should end up with — a final CSS file inside our assets/prod/css folder and a minified JS file insider our assets/prod/js folder.

And this is our final gruntfile.js.

module.exports = function(grunt) {  // Project configuration.
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
connect: {
uses_defaults: {}
},
sass: {
dev: {
options: { sourceMap: true },
files: { 'assets/dev/css/style.css' : 'assets/dev/scss/style.scss' }
},
prod: {
options: {
sourceMap: false,
outputStyle: 'compressed'
},
files: { 'assets/prod/css/style.css' : 'assets/dev/scss/style.scss' }
}
},
watch: {
css: {
files: 'assets/dev/scss/**/*.scss',
tasks: [ 'sass:dev' ],
options: { livereload: true }
}
},
uglify: {
my_targets: {
files: { 'assets/prod/js/output.min.js' : ['assets/dev/js/**/*.js'] }
}
}
});
// Load Grunt plugins
grunt.loadNpmTasks('grunt-contrib-connect');
grunt.loadNpmTasks('grunt-sass');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-contrib-uglify');
// Default task(s).
grunt.registerTask('default', [
'connect',
'watch'
]);
grunt.registerTask('lets-ship-it', [
'sass:prod',
'uglify'
]);
};

You’ve made it!

Conclusion

Setting up Grunt for the first few times can be intimidating and frustrating. I know that for a fact. But you’ve made it till the end. *party*

I hope this article has helped you understand how to set up Grunt and why it’s helpful for the work we’re doing nowadays. I hope it has given you a bit more knowledge about the ins and outs of this tool. There is a ton more to discover about Grunt, about the plugins for Grunt and how to make Grunt do what you want it to do.

I will provide you with a few links which include the GitHub repositories we’ve used during the article (there you can find a lot more options for different plugins) and further reading material. If you have any questions please don’t hesitate to ping me on Twitter @verpixelt. I’m happy to help and/or answer your questions.

If this article was helpful for you I would appreciate if you would hit the recommend button and tell your friends and followers about it. ❤

Links & further reading material

--

--

Kevin Lorenz (@verpixelt)

Frontend developer, type enthusiast, feminist ally, CSS Wizard @IDAGIO_official