Building Am I Down
Intro
Using Yeoman with Grunt (as a build system and to run a server with livereload) was a refreshing developer experience when I first started. But Yeoman is limited to the client side, and so getting the full stack to work was not immediately clear to me. Here's my best attempt to capture everything I learned from building the app, from beginning to end. I'd be a fool not to take suggestions - please let me know your thoughts. (You can comment at: https://news.ycombinator.com/item?id=5913042)
Contents:
- Creating a Yeoman App Inside Express
- Running Express Server and Grunt Server Together
- Doing Authentication in Angular
- Deploying to EC2, Nginx, and A Records
Creating a Yeoman App Inside Express
express #so I created a new express app cd public yo angular --minsafe #and here's a Yeoman app in the public dir
So now, from my root directory, I have my main app.js file that runs the Node server and is initially set up with some routes pointing to some boilerplate views. And then I have the public/app directory in which my Yeoman development code lies, and if I run grunt build, then public/dist will be where my Yeoman production code lies.
Since Yeoman spits out a single page AngularJS app, all we really need is for the index.html to be served to the user. So I removed all of the routing and views that Express gave me, and set up app.js to point to public/app/index.html in development and then public/dist/index.html file in production (to take advantage of Grunt's build system):
app.configure('development', function(){ app.use(express.errorHandler()); app.use(express.static(path.join(__dirname, 'public/app'))); }); app.configure('production', function(){ app.use(express.errorHandler()); app.use(express.static(path.join(__dirname, 'public/dist'))); });
I later added back routing to support authentication and some RESTful type calls.
Running Express Server and Grunt Server Together
I really enjoy doing grunt server, but when your Yeoman app requires an underlying server, grunt server can't do everything.
I could do node app.js to run the server (which runs on port 3000 by default) and then do cd public; grunt server (which somehow detects that it should also run on port 3000 and not the Yeoman-configured default 9000). This seems to work a little bit, but is for some reason kinda unreliable - server calls seem to fail over time and I'm not sure why.
I modified the Gruntfile.js that Yeoman generates and, instead of running grunt server, I now run a new target called css-server to compile and minify Compass CSS and get a pseudo-livereload feel (I'm not using CoffeeScript, so I'm not worrying about that):
grunt.registerTask('server', [ 'clean:server', 'coffee:dist', 'compass:server', 'livereload-start', 'connect:livereload', 'open', 'watch' ]); grunt.registerTask('css-server', [ 'clean:server', 'compass:server', 'cssmin:expressapp', 'livereload-start', 'watch' ]);
There were a few other mods to make this work - I had to create the cssmin:express app target (because I didn't want to run grunt build every time):
cssmin: { dist: { files: { '<%= yeoman.dist %>/styles/main.css': [ '.tmp/styles/{,*/}*.css' ] } }, expressapp: { files: { '<%= yeoman.app %>/styles/main.css': [ '.tmp/styles/{,*/}*.css' ] } } }
and I had to change the watch:Compass task to trigger that cssmin:express app target whenever a Compass file was changed:
compass: { files: ['<%= yeoman.app %>/styles/{,*/}*.{scss,sass}'], tasks: ['compass', 'cssmin:expressapp'] }
You can view source at: https://github.com/davidchang/am-i-down/blob/master/public/Gruntfile.js
Doing Authentication in Angular
Note: there are better articles out there for this - I pretty much just followed http://www.frederiknakstad.com/authentication-in-single-page-applications-with-angular-js/ and http://blog.brunoscopelliti.com/deal-with-users-authentication-in-an-angularjs-web-app
I decided to use Passport and Facebook as the only means to log in. So I set up the app as I normally would (see routes: https://github.com/davidchang/am-i-down/blob/master/routes.js and the Passport config: https://github.com/davidchang/am-i-down/blob/master/passport-config.js), using MongoDb with Mongoose as the back end.
Then, perhaps not the best, I wrote an Angular service (yo angular:service Auth) to make a GET request to /user, which returns a JSON object of the user (if a user is logged in) or a basically empty JSON object (if the user is not logged in). Every time a new Angular controller is run (i.e. any time ng-view does some kind of routing), Auth.isLoggedIn() gets run and redirects if necessary - if the user is not logged in, they should always end up back on the index page, and if they are logged in, they should be redirected to the main user page.
This logic could have arguably been contained in the $routeProvider config, which can run every time a route changes.
Here's the service: https://github.com/davidchang/am-i-down/blob/master/public/app/scripts/services/Auth.js with a simple demonstration of $q for promises in Angular.
Then I wrote a bunch of code and finally got to the point where I could deploy.
Deploying to EC2, Nginx, and A Records
This might be better represented in bullet points - I didn't have an AMI that I knew would really fit (since I wanted nginx with Node), so I started from scratch:
- got an Ubuntu EC2 instance
- installed nginx (using Ubuntu section of http://blog.i-evaluation.com/2013/04/04/lannn-installing-nginx-on-aws/)
- installed node (https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager#ubuntu-mint), since this is a Node app
- installed git, to download the repository from Github
- installed ruby, because I needed it to run grunt build for the Yeoman app(so I could install rvm, compass, following this link here: https://www.digitalocean.com/community/articles/how-to-install-ruby-on-rails-on-ubuntu-12-04-lts-precise-pangolin-with-rvm, then "gem install compass")
- installed grunt-cli and bower globally through npm, also for the Yeoman app to work (npm install -g yo grunt-cli bower)
- installed mongodb for the database and ran screen; mongod; Ctrl+A+D to start the process and hide it in the background
Now to download and run the code:
git clone git://github.com/davidchang/am-i-down.git cd am-i-down npm install cd public npm install, because the Yeoman app had its own large set of NPM dependencies bower install grunt build, to populate public/dist cd ../; npm install -g forever; forever start app.js, to start the Express server (and to do so "forever" - http://blog.nodejitsu.com/keep-a-nodejs-server-up-with-forever)
When i go to my EC2 instance's public IP address, it only works if I connect to port 3000. For this, I'll modify the Nginx config to do a "proxyPass" from requests on port 80 (normal HTTP port) to 3000 (my Express app). I basically followed this: http://stackoverflow.com/questions/5009324/node-js-nginx-and-now
I added:
upstream amidown { server 127.0.0.1:3000; }
then changed location / rule to be this:
location / { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_set_header X-NginX-Proxy true; proxy_pass http://amidown/; #http://localhost:3000; proxy_redirect off; }
then restarted nginx by running:
sudo /etc/init.d/nginx restart
Then, I wanted to point my own subdomain, am-i-down.davidandsuzi.com, to that EC2 instance. So I created an Elastic IP for the instance, then created an A Record that points my subdomain to that elastic IP address. This worked for me.