because, just because
the most basic express application
- Install express on the server's package.json
npm install express
- Require express
var express = require('express');
- Create an express "app"
var app = express();
- Use the app to create a route to the root ('/')
app.get('/', function(req, res) {
res.send('okay, yeah');
});
Note: res.send(something)
is just an express wrapper for res.end(something)
, so it's doing a write(something)
followed by an end()
. It also does some encoding and status code checks.
together
- Take the array from the
ListController
in the angular app
var people = [ ... ];
- Send the json
app.send(people);
- `app.send(JSON.stringify(people));``
- Use
app.json()
to "send" the array
app.json(people);
send()
is actually smart enough to know when you're sending json, but it's nice to be explicit
- Check it in a browser
together
- Use an anonymous function before any routes
app.use(function(req, res, next){ ... })
- this is a pipeline for the request and response streams
- the function will require a
next();
function to be called if you want anything to happen after it - if you place it after any routes, it will never run
- Console log the
date
, themethod
and theurl
console.log((new Date()).toString() + " " + req.method + " " + req.url)
Note: This is not a great logging solution, it's not handling errors or status codes. For that you'll want a real express middleware logging solution. It's just logging is such a nice and easy thing to show you some easy middleware.
together
- Create a route for a listing
app.get('/people', function(req, res){ ... });
- the root route is fine for us right now, but it's not RESTful
- RESTful does have a few rules, like having a single "resource" for making queries against
together
- Look at the angular app
- Run the angular app
npm install
gulp
together
- Add ng-resource to the project
bower install angular-resource
- Include it as a dependency on the app (in app.js)
angular.module('resthitter', ['ui.bootstrap', 'xeditable', 'ngRoute', 'ngResource']).config({ ... });
- Create a
People
service
- create a new file under the services directory,
People.js
- these are normally created as factories
angular.module('resthitter').factory('People', ['$resource', function($resource){ ... }]);
- Add the
$resource
dependency
$resource
is a wrapper for$http
- it is meant to allow you to easily set up hitting restful APIs
- it has 4 main functions:
query()
,get()
,save()
, anddelete()
(andremove()
) - these map to the primary RESTful methods you'll be using (
GET
all,GET
a single,POST
a new entry or change, andDELETE
a single - $resource docs
- Return a
$resource
object
return $resource('http://localhost:7000/people/:id');
- Note: the full url that can be parsed internally by
$resource
to give it the right urls for each RESTful action - Also note:
$resource
is as simple as it can be out of the box, but you can extend it to whatever you need, (for example, if you need it to make aPUT
request against an API)
together
- Inject
People
intoListController
- Use
People.query();
var people = People.query();
- Check your network activity in the browser and you will see it made a request
- Make the edits to have that data display in the template's
ng-repeat
together
- Create a new middleware option
app.use(function(req, res, next){ ... });
- Add the following two headers:
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
- Call
next();
to pass it through to the next middleware
next();
app.use(function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});
Note: Cross Origin Resource Sharing is the most accepted way to subvert the Single Origin Policy, which is a security policy to prevent malicious actions. It's up to the server, and it's generally accepted that you're not going to be allowing * unless you're a truly public API -- usually a read-only API will do that. Usually people will at least limit the sharing to *.domain.com.
together
- Get the value of the current page's id from the
$routeParams
- inject
$routeParams
- current
id
is$routeParams.id
- it's getting this from the
$routeProvider
config in app.js
- it's getting this from the
- Set the value of the person variable to the
$resource.get()
for the current id
var person = People.get({id: $routeParams.id});
- that object parameter is the parameter set in the $resource definition in the service
- so it's equivalent to sending a
GET
request to:http://localhost:7000/people/1
- Make sure the appropriate value gets displayed on the template
together
- Create a new route, similar to the format of our angular
$routeParams
and$resource
app.get('/people/:id', function(req, res){ ... });
- the id is now available on
req.params.id
, courtesy of express
- If we find that
req.params.id
in the people array, return the corresponding person object
forEach
or some other loopish structurepeople.forEach(function(person){ ... });
for(var index = 0; i < people.length; i++){ ... }
- an
if
checking the id of each array elementif(person.id == req.params.id){ ... }
- return the matching element
return person;
- If we don't find it, send a
404
res.status(404).end();
app.get('/people/:id', function(req, res){
people.forEach(function(person){
if(person.id == req.params.id){
res.json(person);
}
});
res.status(404).end();
});
Note: forEach
, (as well as any vanilla loop) is going to be a synchronous process. This means it will be blocking. This is partly why people store data in noSQL solutions when working with Node, so lookups can be offloaded to Mongo or Redis or whatever with an async call. But we can also "simulate asynchronisity" by simply wrapping the interior with a setTimeout()... But then the 404
will get called before it has a chance to check them, so you'll have to handle that somehow. You could use a lib for handling async functions or even a promise pattern -- or just use a counter. But these are the problems running Node has.
var counter = 0;
people.forEach(function(person){
setTimeout(function(){
if(person.id == req.params.id){
res.json(person);
}
counter++;
if(counter == people.length){
// then we've gone through them all and it wasn't found
res.status(404).end();
}
}, 10);
});
together
- Since we have included xeditable, we have some directives available to us
- add the attribute
editable-text
to the name and email texts - use them the same way you would use
ng-model
<h1 editable-text="dc.person.name">{{ dc.person.name }}</h1>
- Hint: wrap the editable field in a
<span>
to avoid the line across the page
- Create a save button
- use
ng-click
to have that button call a save function in your controller
- You can call a save directly from the existing
$resource
you got from the.get()
in that controller
dc.person.$save();
- Note: this returns a promise, so we can chain it with a
.then(success, failure);
- use this
success
function to console log something to let us know if we're succeeding!
dc.person.$save().then(function(){
console.log("success!");
});
Note: this can also be done with a call to the $resource
factory
Name.save({}, dc.person, function(){ console.log("success!"); });
- where the first param is
$resource
params - the second param is the
POST
's body- in this case, we're posting the json for the
person
- in this case, we're posting the json for the
- the third param is a success callback
together
- Set up an
app.post
for the/people
route
app.post('/people', function(req, res){ ... });
- Install
body-parser
npm install body-parser
- this is express middleware
- we need this to get the
POST
data
- Require the body-parser
var bodyParser = require('body-parser');
- Use the body-parser to parse for json
app.use(bodyParser.json());
- now in our routes,
req.body
has been populated with parsed json, assuming that's what was posted
- Get the
person
object fromreq.body
- var person = req.body;
on your own
- Loop through the array in some way (
for()
or.forEach()
or whatever) - Find the match on
id
- Set that index to the post data we just got
- Don't forget to end the response!
res.end();
together
- Inject the
$location
service toDetailController
.controller('DetailController', ['People', '$location', function(People, $location){ ... }]);
$location
is an angular wrapper forwindow.location
- Use the
.then()
promise chained success function to change the location
$location.path('/');
together
- Add a button to the list view
- actually, a link will be more to the point, but we can make it look like a button
<a class="btn btn-info" href="/#/new">new</a>
- Create the new route -- just have it go to the detail template
.when('/new', { ... })
- just populate the route the same as the edit route
- In the Controller, check for an undefined
$routeParam.id
if($routeParam.id === undefined){ ... }
- If it's not defined, create a new
People
object and populate it with some default values
var dc.person = new Person();
dc.person.name = "placeholder";
dc.person.twitter = "@placeholder";
- if it is defined, just do what we were doing before
- Try it
together
- If it gets through the
forEach
, instead of throwing a404
, check if the id is undefined
if(postedPerson.id === undefined){ ... }
- Give the posted person an appropriate id
postedPerson.id = people.length + 1;
- Add the
postedPerson
to thepeople
array
people.push(postedPerson);
- What are the obvious flaws with this?
- hint: if we added a delete...
together
- Add a delete button to the list template with an
ng-click
<button class="btn" ng-click="lc.delete(person.id)">delete</button>
- Create the
delete(id)
function
lc.delete = function(id){ ... };
- Send a
DELETE
request to the server
People.delete({id: id});
- remember, this is like sending a
DELETE
request tohttp://localhost:7000/people/:id
- Try it
- Alter CORS to allow
DELETE
res.header('Access-Control-Allow-Methods', 'POST, GET, PUT, DELETE, OPTIONS');
- Create a delete route on the server
app.delete('/people/:id', function(req, res){ ... })
- Find and delete the entry
people.splice(index);
- don't forget to
res.end()
- Return a
404
if you didn't find it
res.status(404).end();
- In the controller, after a delete, refresh the data
People.delete({id: id}, function(){ ... });
lc.people = People.query();
- Note: in a real scenario, you probably wouldn't want to be hammering the server -- if your
DELETE
came back with a success, you should remove it from the existinglc.people
array- thankfully, this isn't real. Go home.