As JavaScript applications grow in complexity, managing dependencies becomes increasingly challenging. Without a module system, you're left with a mess of global variables, script tags that must be loaded in exactly the right order, and code that's difficult to test and reuse. The traditional approach of manually managing <script> tags breaks down quickly in modern web applications.
This is where RequireJS and the AMD (Asynchronous Module Definition) format come in. RequireJS is a JavaScript file and module loader that implements the AMD API, providing a clean way to define modules, declare dependencies, and load code asynchronously. It transforms how you structure JavaScript applications, bringing the kind of modularity that developers in other languages have enjoyed for years.
I've been using RequireJS for several months now, and it's fundamentally changed how I approach JavaScript architecture. No more global namespace pollution, no more fragile script loading order, and no more wondering which files depend on which. RequireJS makes JavaScript development feel more professional and sustainable.
The Problem with Traditional JavaScript Loading
Before diving into RequireJS, let's understand the problems it solves.
Script Tag Soup
Traditional JavaScript loading looks like this:
<script src="jquery.js"></script>
<script src="underscore.js"></script>
<script src="backbone.js"></script>
<script src="app/models/user.js"></script>
<script src="app/models/post.js"></script>
<script src="app/views/userView.js"></script>
<script src="app/views/postView.js"></script>
<script src="app/router.js"></script>
<script src="app/main.js"></script>
This approach has serious problems:
Order Dependencies: Scripts must be loaded in exactly the right order. If userView.js depends on backbone.js, Backbone must be loaded first. As your application grows, managing this order becomes a nightmare.
Global Namespace Pollution: Every script adds variables to the global scope. You need to be careful not to overwrite existing globals, and tracking which script provides which global is error-prone.
No Explicit Dependencies: Looking at a file, you can't tell what it depends on. Dependencies are implicit, hidden in the HTML. This makes code hard to understand and refactor.
Performance Issues: The browser loads scripts synchronously, blocking page rendering. While you can use async or defer, managing dependencies with asynchronous loading is complex.
The Module Pattern Isn't Enough
Many developers use the module pattern to avoid globals:
var MyApp = MyApp || {};
MyApp.UserView = (function($, Backbone) {
'use strict';
// Private variables and functions
var privateVar = 'secret';
function privateFunction() {
console.log(privateVar);
}
// Public API
return Backbone.View.extend({
initialize: function() {
privateFunction();
}
});
})(jQuery, Backbone);
This is better than dumping everything in the global scope, but it still has problems:
- You're still manually managing script loading order
- Dependencies aren't declared—they're implicit in the function parameters
- There's no built-in way to load modules on demand
- Testing requires loading all dependencies in the right order
What we need is a real module system.
Enter AMD and RequireJS
AMD (Asynchronous Module Definition) is a specification for defining modules in JavaScript. It provides a define function for declaring modules and a require function for loading them. The key feature of AMD is asynchronous loading—modules can be loaded in any order, and dependencies are resolved automatically.
RequireJS is the most popular implementation of the AMD specification. It's a small library (about 15KB minified) that provides:
- Module definition with explicit dependencies
- Asynchronous module loading
- Automatic dependency resolution
- Build optimization for production
- Plugin system for loading non-JavaScript resources
RequireJS 2.0 was released earlier this year, bringing improved performance and new features. It's being used in production by companies including the BBC, LinkedIn, and many others.
Defining Modules with AMD
The fundamental building block of AMD is the define function. Here's the simplest module:
// file: app/greeting.js
define(function() {
'use strict';
return {
sayHello: function(name) {
return 'Hello, ' + name + '!';
}
};
});
This module has no dependencies and exports an object with a sayHello method. The return value becomes the module's public API—anything not returned remains private.
Modules with Dependencies
Most modules depend on other modules. Declare dependencies as an array before the factory function:
// file: app/user.js
define(['jquery', 'underscore'], function($, _) {
'use strict';
function User(data) {
this.data = data;
}
User.prototype.getName = function() {
return this.data.firstName + ' ' + this.data.lastName;
};
User.prototype.save = function() {
return $.ajax({
url: '/api/users/' + this.data.id,
type: 'PUT',
data: JSON.stringify(this.data)
});
};
return User;
});
The dependency array ['jquery', 'underscore'] tells RequireJS to load jQuery and Underscore before executing this module. The loaded modules are passed as arguments to the factory function in the same order.
Named Modules
You can give modules explicit names:
define('app/user', ['jquery', 'underscore'], function($, _) {
// module code
});
However, named modules are less flexible—they're tied to a specific path. Anonymous modules (without a name parameter) are preferred because they're more portable. The build tool will add names automatically during optimization.
Simplified CommonJS Wrapping
If you prefer the CommonJS-style syntax used in Node.js, AMD supports it:
define(function(require, exports, module) {
'use strict';
var $ = require('jquery');
var _ = require('underscore');
function User(data) {
this.data = data;
}
// Methods...
exports.User = User;
});
RequireJS parses the require calls and loads dependencies before executing the module. This syntax is more familiar to Node.js developers but is slightly less efficient because RequireJS must parse the function to find dependencies.
Loading Modules with require
While define creates modules, require loads and uses them:
require(['app/user'], function(User) {
var user = new User({
id: 1,
firstName: 'John',
lastName: 'Doe'
});
console.log(user.getName()); // "John Doe"
user.save().done(function() {
console.log('User saved!');
});
});
The require function is similar to define, but it's used at the application level to start execution rather than to define reusable modules. Think of require as the entry point that kicks off your application.
Loading Multiple Modules
Load as many modules as needed:
require([
'jquery',
'underscore',
'backbone',
'app/router',
'app/views/mainView'
], function($, _, Backbone, Router, MainView) {
'use strict';
// Initialize application
var router = new Router();
var mainView = new MainView();
Backbone.history.start();
});
RequireJS loads all dependencies in parallel (where possible) and executes the callback once everything is loaded. This parallel loading improves performance compared to sequential script loading.
Configuring RequireJS
RequireJS needs configuration to work with your application structure. Configuration happens through requirejs.config():
requirejs.config({
// Base URL for all module paths
baseUrl: 'js/lib',
// Paths for specific modules
paths: {
'jquery': 'jquery-1.8.2.min',
'underscore': 'underscore-1.4.1.min',
'backbone': 'backbone-0.9.2.min',
'app': '../app'
},
// Shim configuration for non-AMD libraries
shim: {
'underscore': {
exports: '_'
},
'backbone': {
deps: ['underscore', 'jquery'],
exports: 'Backbone'
}
}
});
Configuration Options
baseUrl: The root path for all module IDs. Module paths are relative to this. If not specified, the path to the HTML page loading RequireJS is used.
paths: Map module IDs to paths. Useful for:
- Aliasing long paths to shorter names
- Pointing to CDN locations with local fallbacks
- Separating library code from application code
Example with CDN and fallback:
paths: {
'jquery': [
'//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min',
'lib/jquery-1.8.2.min'
]
}
If loading from the CDN fails, RequireJS tries the local path.
shim: Configure dependencies and exports for libraries that don't use AMD. Many popular libraries (jQuery, Underscore, Backbone) don't support AMD natively yet. The shim tells RequireJS how to load them:
shim: {
'backbone': {
deps: ['underscore', 'jquery'], // Load these first
exports: 'Backbone' // This global is the module value
},
'jquery.plugin': {
deps: ['jquery'], // Depends on jQuery
exports: 'jQuery.fn.plugin' // Plugin adds to jQuery
}
}
waitSeconds: How long to wait before giving up on loading a script (default: 7 seconds). Set to 0 to disable timeout.
map: For when you need different versions of a module for different parts of your application:
map: {
'app/feature1': {
'jquery': 'jquery-1.7.2'
},
'app/feature2': {
'jquery': 'jquery-1.8.2'
}
}
Structuring a RequireJS Application
A typical RequireJS application structure looks like this:
project/
├── index.html
└── js/
├── lib/
│ ├── require.js
│ ├── jquery.js
│ ├── underscore.js
│ └── backbone.js
├── app/
│ ├── models/
│ │ ├── user.js
│ │ └── post.js
│ ├── views/
│ │ ├── userView.js
│ │ └── postView.js
│ ├── collections/
│ │ └── users.js
│ ├── router.js
│ └── main.js
└── config.js
The HTML Entry Point
Your HTML file only needs one script tag:
<!DOCTYPE html>
<html>
<head>
<title>My Application</title>
</head>
<body>
<div id="app"></div>
<!-- Load RequireJS and start the application -->
<script data-main="js/config" src="js/lib/require.js"></script>
</body>
</html>
The data-main attribute tells RequireJS which file to load first. This file typically contains configuration and bootstraps the application.
The Configuration File
js/config.js sets up RequireJS and starts the application:
requirejs.config({
baseUrl: 'js/lib',
paths: {
'app': '../app',
'jquery': 'jquery-1.8.2.min',
'underscore': 'underscore-1.4.1.min',
'backbone': 'backbone-0.9.2.min'
},
shim: {
'underscore': { exports: '_' },
'backbone': {
deps: ['underscore', 'jquery'],
exports: 'Backbone'
}
}
});
// Load the main application module
require(['app/main']);
The Application Module
js/app/main.js is your application's entry point:
define([
'jquery',
'underscore',
'backbone',
'app/router',
'app/views/mainView'
], function($, _, Backbone, Router, MainView) {
'use strict';
var App = {
initialize: function() {
// Create main view
this.mainView = new MainView({
el: '#app'
});
// Create router
this.router = new Router();
// Start routing
Backbone.history.start({ pushState: true });
console.log('Application initialized');
}
};
// Start the application
App.initialize();
return App;
});
Example Module: Router
js/app/router.js:
define([
'jquery',
'underscore',
'backbone',
'app/views/homeView',
'app/views/userView'
], function($, _, Backbone, HomeView, UserView) {
'use strict';
var AppRouter = Backbone.Router.extend({
routes: {
'': 'home',
'users/:id': 'showUser'
},
home: function() {
var view = new HomeView();
$('#app').html(view.render().el);
},
showUser: function(id) {
var view = new UserView({ userId: id });
$('#app').html(view.render().el);
}
});
return AppRouter;
});
RequireJS Plugins
RequireJS supports plugins that extend what can be loaded as a module. Plugins are specified with a prefix in the module ID.
Text Plugin
The text plugin loads text files (templates, CSS, etc.) as strings:
define([
'jquery',
'underscore',
'backbone',
'text!templates/userView.html'
], function($, _, Backbone, userTemplate) {
'use strict';
var UserView = Backbone.View.extend({
template: _.template(userTemplate),
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
return UserView;
});
The template file templates/userView.html:
<div class="user">
<h2><%= name %></h2>
<p><%= email %></p>
</div>
Domready Plugin
The domready plugin executes code when the DOM is ready:
require(['domready!'], function() {
// DOM is ready
document.getElementById('message').innerHTML = 'Ready!';
});
Custom Plugins
You can write custom plugins to load any resource type. A plugin is just a module that exports a load function:
define({
load: function(name, req, onLoad, config) {
// Load the resource and call onLoad when ready
var url = 'resources/' + name + '.json';
req(['jquery'], function($) {
$.getJSON(url, function(data) {
onLoad(data);
});
});
}
});
Use it like this:
require(['json!config'], function(config) {
console.log(config);
});
Optimization with r.js
One concern with RequireJS is the number of HTTP requests in development—each module is a separate file. For production, RequireJS provides r.js, an optimization tool that concatenates and minifies modules.
Installing r.js
r.js is a Node.js command-line tool. Install it with npm:
npm install -g requirejs
Or use it directly with Node:
node r.js -o build.js
Build Configuration
Create a build configuration file build.js:
({
appDir: './js',
baseUrl: 'lib',
dir: './js-built',
modules: [
{
name: '../config'
}
],
paths: {
'app': '../app',
'jquery': 'jquery-1.8.2.min',
'underscore': 'underscore-1.4.1.min',
'backbone': 'backbone-0.9.2.min'
},
shim: {
'underscore': { exports: '_' },
'backbone': {
deps: ['underscore', 'jquery'],
exports: 'Backbone'
}
},
optimize: 'uglify2',
optimizeCss: 'standard',
removeCombined: true
})
Build Options
appDir: The root directory of your application.
baseUrl: Same as the RequireJS baseUrl config.
dir: Output directory for the optimized files.
modules: Which modules to optimize. Usually your main entry point. The optimizer traces dependencies and bundles everything into one file.
optimize: Which optimizer to use:
uglify: UglifyJS 1.xuglify2: UglifyJS 2.x (better optimization)closure: Google Closure Compilernone: No minification (just concatenation)
optimizeCss: Whether to optimize CSS files (none, standard, or standard.keepLines).
removeCombined: Remove files that were combined into a build file.
Running the Build
Execute the build:
r.js -o build.js
This creates an optimized version in js-built/ with all modules concatenated and minified. Instead of dozens of HTTP requests, your application loads in one or two requests.
Build Profiles for Different Environments
Create multiple build configurations for different scenarios:
// build-mobile.js
({
baseUrl: 'js/lib',
name: '../app/mobile-main',
out: 'mobile-optimized.js'
})
// build-desktop.js
({
baseUrl: 'js/lib',
name: '../app/desktop-main',
out: 'desktop-optimized.js'
})
This lets you create optimized builds for specific platforms or features.
Advanced Patterns
Circular Dependencies
Sometimes two modules depend on each other:
// a.js
define(['b'], function(b) {
return { name: 'a', other: b };
});
// b.js
define(['a'], function(a) {
return { name: 'b', other: a };
});
This causes a circular dependency. RequireJS handles this by returning an empty object initially. To work around it, use require inside the module:
// a.js
define(['require', 'b'], function(require, b) {
var a = { name: 'a' };
a.other = b;
return a;
});
// b.js
define(['require', 'a'], function(require, a) {
var b = { name: 'b' };
b.other = a;
return b;
});
Or better yet, refactor to eliminate the circular dependency.
Loading Modules Dynamically
Load modules conditionally or on demand:
define(['jquery'], function($) {
'use strict';
return {
loadFeature: function() {
require(['app/feature'], function(feature) {
feature.initialize();
});
}
};
});
This is useful for code splitting—loading features only when needed rather than upfront.
JSONP Service
Load JSONP services as dependencies:
require([
'http://api.example.com/data?callback=define'
], function(data) {
console.log(data);
});
The service must wrap the response in define(). This pattern is useful for loading data from external APIs.
Testing RequireJS Modules
RequireJS modules are easy to test because dependencies are explicit and can be mocked.
Testing with Jasmine
// Test file
define([
'app/user',
'jquery'
], function(User, $) {
describe('User', function() {
var user;
beforeEach(function() {
user = new User({
id: 1,
firstName: 'John',
lastName: 'Doe'
});
});
it('should return full name', function() {
expect(user.getName()).toBe('John Doe');
});
it('should save via AJAX', function() {
var server = sinon.fakeServer.create();
server.respondWith('PUT', '/api/users/1', [
200,
{ 'Content-Type': 'application/json' },
JSON.stringify({ success: true })
]);
user.save();
server.respond();
expect(server.requests.length).toBe(1);
server.restore();
});
});
});
Test Runner HTML
<!DOCTYPE html>
<html>
<head>
<title>Tests</title>
<link rel="stylesheet" href="lib/jasmine.css">
<script src="lib/jasmine.js"></script>
<script src="lib/jasmine-html.js"></script>
<script data-main="test-config" src="lib/require.js"></script>
</head>
<body>
</body>
</html>
Test Configuration
// test-config.js
requirejs.config({
baseUrl: '../js/lib',
paths: {
'app': '../app',
'specs': '../specs'
},
shim: {
'jasmine-html': {
deps: ['jasmine'],
exports: 'jasmine'
}
}
});
require([
'jasmine-html',
'specs/userSpec'
], function(jasmine) {
jasmine.getEnv().addReporter(new jasmine.HtmlReporter());
jasmine.getEnv().execute();
});
Common Patterns and Best Practices
One Module Per File
Each file should define exactly one module. This keeps code organized and makes dependency graphs clearer.
Anonymous Modules
Use anonymous modules (no name parameter) so modules are portable:
// Good
define(['jquery'], function($) {
// ...
});
// Avoid
define('app/myModule', ['jquery'], function($) {
// ...
});
The optimizer adds names during the build process.
Return Values
Modules should return their public API:
define(function() {
// Private
var secret = 'private';
function privateFunction() {
console.log(secret);
}
// Public
return {
publicMethod: function() {
privateFunction();
}
};
});
Anything not returned remains private to the module.
Avoid Deep Nesting
Keep module paths relatively flat. Instead of:
app/features/userManagement/views/userProfile/editView.js
Use:
app/views/userProfileEditView.js
Deep nesting makes module IDs verbose and harder to maintain.
Use Relative Module IDs Carefully
Modules can use relative IDs:
define(['./sibling', '../parent/cousin'], function(sibling, cousin) {
// ...
});
This works but makes modules less portable. Use absolute paths from baseUrl for better clarity.
Configuration in One Place
Keep all RequireJS configuration in one file (usually config.js). Don't scatter requirejs.config() calls throughout your application.
Comparing RequireJS to Alternatives
Script Loaders vs. Module Loaders
Script loaders (like LABjs or $script.js) load JavaScript files asynchronously but don't provide module definition or dependency management. RequireJS is a module loader—it both loads scripts and manages dependencies.
CommonJS and Node.js
Node.js uses CommonJS modules with synchronous require(). CommonJS works well on the server where file I/O is fast, but synchronous loading is problematic in browsers. AMD is designed specifically for asynchronous loading in browsers.
That said, RequireJS supports the CommonJS wrapping format, so you can write Node.js-style code that runs in the browser.
Manually Managing Dependencies
Without a module loader, you manually manage script tags and loading order. This works for small projects but becomes unmaintainable as applications grow. RequireJS provides automatic dependency management that scales.
When to Use RequireJS
RequireJS is ideal for:
- Single-page applications with many JavaScript files
- Applications with complex dependency graphs
- Projects where you want to load code on demand
- Teams that need clear module boundaries
It might be overkill for:
- Simple websites with minimal JavaScript
- Projects with just a few scripts
- Prototypes where structure isn't a priority yet
Real-World Example: Todo Application
Let's build a complete todo application to demonstrate RequireJS in practice.
HTML
<!DOCTYPE html>
<html>
<head>
<title>Todo App</title>
<link rel="stylesheet" href="css/app.css">
</head>
<body>
<div id="app"></div>
<script data-main="js/config" src="js/lib/require.js"></script>
</body>
</html>
Configuration
// js/config.js
requirejs.config({
baseUrl: 'js/lib',
paths: {
'app': '../app',
'jquery': 'jquery-1.8.2.min',
'underscore': 'underscore-1.4.1.min',
'backbone': 'backbone-0.9.2.min',
'text': 'text'
},
shim: {
'underscore': { exports: '_' },
'backbone': {
deps: ['underscore', 'jquery'],
exports: 'Backbone'
}
}
});
require(['app/main']);
Main Application Module
// js/app/main.js
define([
'jquery',
'backbone',
'app/views/appView'
], function($, Backbone, AppView) {
'use strict';
var app = new AppView();
return app;
});
Todo Model
// js/app/models/todo.js
define(['backbone'], function(Backbone) {
'use strict';
var Todo = Backbone.Model.extend({
defaults: {
title: '',
completed: false
},
toggle: function() {
this.save({ completed: !this.get('completed') });
}
});
return Todo;
});
Todo Collection
// js/app/collections/todos.js
define([
'backbone',
'app/models/todo'
], function(Backbone, Todo) {
'use strict';
var TodoList = Backbone.Collection.extend({
model: Todo,
localStorage: new Backbone.LocalStorage('todos'),
completed: function() {
return this.where({ completed: true });
},
remaining: function() {
return this.where({ completed: false });
}
});
return new TodoList();
});
Todo View
// js/app/views/todoView.js
define([
'jquery',
'underscore',
'backbone',
'text!templates/todo.html'
], function($, _, Backbone, todoTemplate) {
'use strict';
var TodoView = Backbone.View.extend({
tagName: 'li',
template: _.template(todoTemplate),
events: {
'click .toggle': 'toggleCompleted',
'click .destroy': 'destroy'
},
initialize: function() {
this.listenTo(this.model, 'change', this.render);
this.listenTo(this.model, 'destroy', this.remove);
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
this.$el.toggleClass('completed', this.model.get('completed'));
return this;
},
toggleCompleted: function() {
this.model.toggle();
},
destroy: function() {
this.model.destroy();
}
});
return TodoView;
});
App View
// js/app/views/appView.js
define([
'jquery',
'underscore',
'backbone',
'app/collections/todos',
'app/views/todoView',
'text!templates/app.html'
], function($, _, Backbone, todos, TodoView, appTemplate) {
'use strict';
var AppView = Backbone.View.extend({
el: '#app',
template: _.template(appTemplate),
events: {
'keypress #new-todo': 'createOnEnter',
'click #clear-completed': 'clearCompleted'
},
initialize: function() {
this.listenTo(todos, 'add', this.addOne);
this.listenTo(todos, 'reset', this.addAll);
this.listenTo(todos, 'all', this.render);
this.render();
todos.fetch();
},
render: function() {
this.$el.html(this.template({
total: todos.length,
completed: todos.completed().length,
remaining: todos.remaining().length
}));
this.$todoList = this.$('#todo-list');
this.addAll();
return this;
},
createOnEnter: function(e) {
if (e.which !== 13) return;
var $input = this.$('#new-todo');
var value = $input.val().trim();
if (!value) return;
todos.create({ title: value });
$input.val('');
},
addOne: function(todo) {
var view = new TodoView({ model: todo });
this.$todoList.append(view.render().el);
},
addAll: function() {
this.$todoList.empty();
todos.each(this.addOne, this);
},
clearCompleted: function() {
_.invoke(todos.completed(), 'destroy');
}
});
return AppView;
});
This example demonstrates:
- Clear module boundaries with explicit dependencies
- Loading templates with the text plugin
- Organizing code by type (models, collections, views)
- A scalable architecture that grows with the application
Migration Strategies
Adopting RequireJS doesn't require rewriting everything at once. Migrate incrementally:
Step 1: Load RequireJS
Add RequireJS to your project and configure it to work with your existing code:
requirejs.config({
baseUrl: 'js',
paths: {
'legacy': 'legacy-app'
}
});
require(['legacy/app'], function() {
// Existing app loaded
});
Step 2: Shim Existing Libraries
Use shim configuration for libraries that aren't AMD-compatible:
shim: {
'legacy/myLibrary': {
exports: 'MyLibrary'
}
}
Step 3: Convert Modules Gradually
Convert files to AMD modules one at a time, starting with leaf nodes (modules with few dependencies). Test thoroughly after each conversion.
Step 4: Optimize When Ready
Once your modules are converted, set up the optimizer for production builds.
This gradual approach reduces risk and lets you learn RequireJS while maintaining a working application.
Next Steps
RequireJS and AMD bring real modularity to JavaScript, transforming how you structure applications. Instead of global variables and fragile script loading, you get explicit dependencies, automatic loading, and proper encapsulation. The development experience is cleaner—you know exactly what each module needs and provides.
The learning curve is gentle. Start by converting a few files to AMD modules. Configure RequireJS to work with your existing libraries using shim config. Once you're comfortable with the basics, set up the optimizer for production builds. The incremental approach works well—you don't need to rewrite everything at once.
For your next JavaScript project, consider starting with RequireJS from the beginning. Structure your application as modules, declare dependencies explicitly, and let RequireJS handle the loading. As your application grows, you'll appreciate the organization and maintainability that modularity provides.
The RequireJS documentation at requirejs.org is excellent—clear examples, detailed API docs, and explanations of advanced features. The optimizer documentation covers build configuration in depth. And the RequireJS Google Group is active if you have questions.
JavaScript applications are growing more complex, and the tools we use must evolve with them. RequireJS represents a significant step forward in JavaScript architecture. If you're building anything beyond a simple website, modular JavaScript with RequireJS is worth serious consideration. Try it on a small project and see how it changes your approach to JavaScript development.