This tutorial is designed to take a developer from zero to MEAN in roughly 2-3 hours. Knowledge of HTML, CSS and JavaScript is encouraged, but the exercise is completely do-able from a minimal knowledge perspective.
- Environment Setup (
git
,node
,npm
,mongo
,angular
) - Demonstration of the Finished Product
- Building a Stand Alone Server (Node)
- Starting from the Front-End (Angular)
- Retrieving Dummy Data from the Back-End (Express)
- Tying the Back-End to a Data Source (MongoDB)
- Review of the Finished Product
- Publishing to the Cloud
This tutorial assumes you have at least the following installed:
To configure each of these, please follow the best practices for your system based on the respective tutorials provided above.
When you are completed, the following commands should output similar results (version numbers may vary):
git --version
git version 2.5.0
node --version && npm --version
v5.1.0
3.3.12
mongod --version
db version v2.6.10
Clone this repository using git clone https://github.com/LenPayne/gettingMEAN MYAPP
where MYAPP
is the name of your app.
When this command has completed, you should be able to run the following to begin the server:
cd MYAPP
npm install
node server.js
At this point, you will be able to navigate to http://localhost:3000 and see the finished product.
To build this from scratch, perform git checkout Phase-0
and perform the
following steps.
Create a file called server.js
and insert the following code:
var LISTENING_PORT = 3000;
var express = require('express');
var app = express();
app.use(express.static('public'));
app.use('/angular', express.static('node_modules/angular'));
app.listen(LISTENING_PORT, function() {
console.log('Now listening on http://localhost:' + LISTENING_PORT);
});
These steps achieve the following goals:
- Configures a variable called
LISTENING_PORT
for later use. - Imports and configures the start of an ExpressJS application.
- Allows the ExpressJS app to use the
public
folder to store documents that will be publicly accessible on the web. - Allows the ExpressJS app to provide a web-facing route to the
angular
folder, so that AngularJS can be reached after being installed bynpm
. - Sets the ExpressJS app to listen on
LISTENING_PORT
and provides feedback to the user.
Now that we've said "the public
folder is publicly accessible on the web" we
must create this public
folder, and give it some basic content.
Inside the public
folder, create the file index.html
and give it the
following content:
<!DOCTYPE html>
<html>
<head>
<title>Getting MEAN</title>
</head>
<body>
<div ng-app>
<p>Name: <input ng-model="name" />
<h1>Hello {{name}}</h1>
</div>
<script src="/angular/angular.min.js"></script>
</body>
</html>
At this point, at the command prompt in your MYAPP
folder, you should be able
to execute node server.js
and it will inform you that it is listening on
http://localhost:3000.
By navigating to that page with a web browser, you should see:
- an empty textbox, and
- The word "Hello".
If you type anything into the textbox, it will appear next to Hello. Type the word "World" into the textbox and watch the display update. Welcome to Angular.
At this stage, the next phase is to build the front-end, so that you can shape what the user will see in the final product. The goal of this project is to provide a database-backed microblog. Basically, a Twitter clone.
So we need to build:
- A form to make a new microblog post
- A display for a list of all posts on the system
For simplicity's sake, we won't be adding full authentication (ie- login) at this stage.
<div>
<div>Name: <input ng-model="author" /></div>
<div>Message: <textarea ng-model="content"></textarea></div>
<div><button ng-click="addPost()">Post</button></div>
</div>
Note that this does not use a <form>
element. The <form>
element has special
meaning to browsers. We're going to hijack the button manually and want to avoid
any default behaviour.
We created: 1) an <input>
that ties to an Angular object called author
; 2) a
<textarea>
that ties to an Angular object called content
; and 3) a <button>
that asks Angular to call an addPost()
function when it's clicked.
<div ng-repeat="post in posts">
<p>{{post.content}}</p>
<h5>{{post.author}}</h5>
</div>
This uses Angular's ng-repeat
system to declare that we're going to read all
of the post
objects out of a collection of posts
. For each of the post
objects, we create a <p>
tag, and an <h5>
holding the post content
and
author
respectively. This means, if there are 100 post
objects in the posts
collection, there will be 100 <div>
tags, with 100 <p>
and <h5>
tags too.
In Angular, we use a paradigm called "Model-View-Controller" or MVC. So far, we have been building the "View" part, and accessing data held in our "Model". Now we're going to address the "Controller", which is a kind of central station for communicating between the parts. In our previous steps, an implicit Controller has been used. Now, we're going to declare an explicit Controller.
Create a new folder in the project called js
, inside that create a new file
called app.js
(ie- create js/app.js
). Include the following code in this new
file:
var postApp = angular.module('postApp', []);
postApp.controller('postCtrl', function($scope) {
$scope.posts = [];
$scope.addPost = function() {
var newPost = { author: $scope.author, content: $scope.content};
$scope.posts.push(newPost);
$scope.author = '';
$scope.content = '';
};
});
Breaking this down:
- We declare an Angular
module
that we callpostApp
. - We give
postApp
acontroller
that we callpostCtrl
: it accepts the$scope
service. - We declare an object on the
$scope
calledposts
that starts as an empty collection (ie-[]
). This is our "Model." - We declare a function on the
$scope
calledaddPost
that: 1) builds anewPost
object that holds the currentauthor
andcontent
data in the "Model"; 2) pushes thenewPost
onto theposts
collection. - Sets
author
andcontent
back to empty.
We need to tie this code back to the HTML page. In index.html
we need a new
<script>
tag. Below the existing <script>
tag add the following:
<script src="/js/app.js"></script>
Now that we have an Angular Module, and a Controller, we also need to tag them
into the HTML using ng-app
and ng-controller
attributes.
Modify the existing <body>
and opening <div>
tags to be:
<body ng-app="postApp">
<div ng-controller="postCtrl">
... The Form and The List Should Be Here ...
</div>
<script src="/angular/angular.min.js"></script>
<script src="/js/app.js"></script>
</body>
After completing all of these steps (and only after completing ALL of these steps,) you will be able to navigate to http://localhost:3000/ and be able to add posts to your collection.
At this point, we have a system that holds posts in active browser memory. So long as the browser window/tab stays open, the application will display the same posts and allow you to add more to the list. However, there is no concurrency: if you navigate to the app from another browser, you will not see the same list of posts. We will work to fix that next.
On the NodeJS side, we will use the Express framework to create an endpoint to retrieve a list of posts.
First, we will declare a posts
collection on the server.js
file. Near the
top of the file, add the line:
var posts = [];
This declares a globally-available collection of posts
objects.
Now, between the app.use
and the app.listen
lines, add the following block:
app.get('/posts', function(request, response) {
response.send(posts);
});
This provides an endpoint for the HTTP GET method on the /posts
URL. It
declares a function that takes in a request
and a response
object (this is
how Express works.) The response
has a method to send
out some data, and at
this point we return posts
.
Whenever you make changes to server.js
, you will need to go to your command
prompt and cancel the current-running node
process by pressing Ctrl-C.
Re-run the process with node server.js
and you should be able to navigate to
http://localhost:3000/posts and receive []
as a response.
To enable adding Posts, we will want to include a NodeJS middleware called
body-parser
, that parses the body of a request into objects that we can use.
This is a multi-step process. First, we must change our package.json
file to
declare that body-parser
is a required dependency. Modify your "dependencies"
object to include the key-value pair: "body-parser" : "1.x"
.
"dependencies" : {
"body-parser" : "1.x",
...
}
Once you save this file, you must re-run npm install
at the command line.
Now you can go back to server.js
and declare that body-parser
is required.
Do this between the var express
and var app
lines.
var express = require('express');
var bodyParser = require('body-parser');
var app = express();
Now, we must declare that the app
actually USES this middleware by adding the
following line after the other app.use
lines:
app.use(bodyParser.json());
Finally, we can make an app.post
block for /posts
that will receive a JSON
object from Angular and add it to our posts
collection.
app.post('/posts', function(request, response) {
var post = request.body;
posts.push(post);
response.send(post);
});
At this point, if we re-start the node server.js
process, we will be able to
send a POST to our new server. This can be tested with Postman,
but we're going to barrel ahead and move back to Angular to add posts to our list.
On the Angular side, we must use the $http
service to interact with our
Express endpoints. First, we must declare that we use the $http
service in our
controller declaration. We add $http
as a parameter on the function in app.js
as so:
postApp.controller('postCtrl', function($scope, $http)
The rest of the controller requires significant reworking as well. It may be easiest to do a full re-write:
postApp.controller('postCtrl', function($scope, $http) {
$scope.posts = [];
$scope.getPosts = function() {
$http.get('/posts').then(
function(response) {
$scope.posts = response.data;
},
function(response) {
console.log(response);
});
};
$scope.addPost = function() {
var newPost = {
author: $scope.author,
content: $scope.content
};
$http.post('/posts', newPost).then(
$scope.getPosts,
function(response) {
console.log(response);
});
};
$scope.getPosts();
});
Breaking this down we:
- Leave the default
posts
collection empty. - Set up a
getPosts
function that uses the$http
service toget
data from our/posts
resource. Thisresponse.data
is then applied to ourposts
collection. The remainder of this block is error reporting. - Modify our
addPost
function to send over the$http
service topost
data to our/posts
resource. On success, it calls ourgetPosts
function. - Call out to
getPosts
for the first time to make sure ourposts
information is updated when we first load the page.
At this point, we should have a working site that allows us to add and read
posts to our service, that presents the same information across every connected
browser (try it and see: run node server.js
and go to http://localhost:3000 in
another browser or in incognito mode.)
Now that we have our AngularJS app transferring data to/from the NodeJS/Express server, we are able to show our list of Posts to everyone. You can navigate to the page in different tabs, and different browsers, and everyone sees the same Posts.
Unfortunately, there is a small quirk: when the node server.js
process closes,
all of the data that is stored in memory disappears.
To fix this problem, we need to persist that data to a long-term datastore:
MongoDB. We will simplify our interaction with MongoDB by using a tool called
mongoose
.
In the server.js
file, add the following lines up near the top (anywhere above
the app.get
and app.post
lines):
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/posts');
var postSchema = mongoose.Schema({author: String, content: String});
var Post = mongoose.model('Post', postSchema);
Breaking this down:
- We import the
mongoose
package (which was already imported as part of thepackage.json
file, and pulled in bynpm install
.) - We connect to the MongoDB server running locally, and specifically to the
posts
collection. - We build a
Schema
that tellsmongoose
exactly what a goodPost
is going to look like (ie- it has an author and some content.) This helps prevent us from saving bad data into our datastore. - We create a data-model named
Post
that uses this schema, and gives us access to a great number of tools likefind
,save
, and others.
In addition to connecting to the datastore, we need to actually USE it in our
program. The first thing we'll do is modify our app.get
block so that it finds
the data stored in MongoDB:
app.get('/posts', function(request, response) {
Post.find(function(err, posts) {
if (err)
response.status(500).send('Failed to retrieve posts');
else
response.send(posts);
});
});
Breaking this down:
- We use the
Post
model object to call itsfind
method, that will return all of the objects in the related collection. - If there's an error, we warn the user by setting the status of our
response
to be 500, and giving a bit of feedback on what happened. - If there was no error, though, we simply respond with the full
posts
object straight out ofmongoose
. This will be presented already as a JSON array, ready for AngularJS to parse.
Our last step of data persistence is to save the Posts that come in from the
outside world. We do this by modifying our app.post
block:
app.post('/posts', function(request, response) {
var post = new Post(request.body);
post.save(function (err) {
if (err) {
response.status(500).send('Failed to save post: ' + request.body);
} else {
response.send(post);
}
});
});
Breaking this down:
- We create a new
Post
model object, and feed it therequest.body
, which in our case is a JSON object that holds the author and content. - That new
Post
model object has a save method that will talk tomongoose
in the background for us and either succeed or fail. - If it fails, we give some feedback to the user, similar to what we did with
app.get
previously. - If it succeeds, we send the
post
object back as a sign that everything was A-Okay.
At this point, the code is complete and correct. However, we need to make sure the environment around the code is functional. Namely, we must make sure that MongoDB is turned on.
This varies by environment, so I will refer you to MongoDB's references for Windows, OS X, and Linux.
To verify that your MongoDB instance is configured and running, you should be
able to run mongo
at the command line with no parameters and it will connect
to your locally running instance. It will look something like this:
$ mongo
MongoDB shell version: 2.6.10
connecting to: test
> use posts;
switched to db posts
> exit;
If you already have some posts in your datastore, you can run db.posts.find();
and it will return all of the content in JSON format.
Now you can cancel (Ctrl-C) and re-run your node server.js
process, and it
should work, and persist the data to the database. You can test this by
navigating to http://localhost:3000 and inserting some data. When you cancel and
re-run the application, the data should still be available in your app.
Congratulations! Your project is now complete! It works! Of course, there's always more to add...
- You can make the user interface more appealing using Angular-UI or some custom CSS.
- You can add features like editing and deleting posts by adding elements to
the AngularJS forms in
index.html
that send different messages through the controller inapp.js
back to theserver.js
. Specifically, we often use$http.put()
to edit messages, and$http.delete()
to send a message to delete. - This application does not scale super-well: it sends the WHOLE COLLECTION every time you do anything. On a test basis, a handful is fine, but if the collection of posts measures in hundreds or thousands, this can slow the application down. We can implement paging in the UI and in the Server to make this process easier.
- You can add a login and authentication process of your own devising, or tie into an OAuth provider like Google, Facebook or Twitter.
The world is yours for the taking, go get MEAN about it!