Design, build, test and deploy a RESTful API for a blogging app with authentication using Node
and Mongo
HTTPie
: Amazing CLI to play with APIs.
- [Parse] (https://parse.com/docs/rest/guide): A nice real-world example of a REST API.
Robomongo
: Native and cross-platformMongoDB
manager.OrientDB
: 2nd Generation Distributed Graph Database.Moongose
API
Docs
BlueBird.js
: Very performantpromises
library.Moongose
Built-in Promises
- JWT:
JWT.IO
allows you to decode, verify and generateJWT
. passport
: Simple, unobtrusive authentication forNode
jsonwebtoken
: An implementation of JSON Web Tokens forNode
.express-jwt
:Express
middleware
that validates aJWT
and set thereq.user
with the attributes
cors
:Express
middleware
to enableCross Origin Resource Sharing
(CORS
)method-override
: Lets you useHTTP
verbs such asPUT
orDELETE
in places where the client doesn't support itnode-slug
:slug
ifies even utf-8 chars- Scott's Notes
- Scott's Code
- Scott's frontend
- James Kyle's Example Node Server: Example Node Server with Babel 6.
- Boilerplate Express/ES2015/Mocha/MongoDB API:
Express
,ES2015
,Mocha
,MongoDB
API
Boilerplate - babel-preset-es2015-node5: Babel preset to make node@5 fully ES2015 compatible.
- Misunderstanding ES6 Modules, Upgrading Babel, Tears, and a Solution
Node
is basically a way to run javascript outside the context of the browser.- To get started just type
node
in your terminal and you'll have a fullREPL
.
Use require()
to get access to:
- Built in
node
modules. - 3rd party
npm
modules. - Our own modules.
// built in node module
const path = require('path');
// 3rd party module downloaded into node_modules/
const _ = require('lodash');
// a module we created in another file
const myModule = require('./path/to/my/module');
Use the exports
object to expose our own node
modules.
Selecting individual properties
// yourfile.js
exports.setup = () => {};
exports.enable = () => {};
exports.ready = true;
Using module.exports
// yourfile.js
module.exports = {
setup: () => {},
enable: () => {},
ready: true
};
- When using
exports
the module will be exported as anobject
. - When using
module.exports
you can export whatever you want (anobject
, afunction
, anumber
, ...).
There's still no native support, yet, but it's coming:
âś” node --version
v5.7.0
âś” node --v8-options | grep "in progress"
--harmony_modules (enable "harmony modules" (in progress))
Use babel
meanwhile
When Node
executes your files it previously wrap them in IIFE
s (Immediately Invoked Function Expression):
(function (module, exports, __dirname, ....) {
YOUR CODE GOES HERE
}())
Node
has a built-inhttp
module that allows us to create servers, but you need to write too much code for that.Express
framework:- Sits on top of
Node
- Uses the
http
Node
module. - Is composed of
routing
andmiddleware
. - Allows to register
callbacks
forroutes
with differenthttp
verbs.
- Sits on top of
- Other popular choices are
Koa
,Hapi
andSails
.
const express = require('express');
const app = express();
// on GET request to the url
app.get('/todos', (req, res) => {
});
// on POST request to the same url
app.post('/todos', (req, res) => {
});
// start server on port 3000
app.listen(3000);
app.get('/todos', (req, res) => {
res.json(todos);
});
res.json
:
- sends back a
json
response
. - converts
null
andundefined
tojson
(although it's not validjson
).
app.get('/todos', (req, res) => {
res.send(todos);
});
res.send
:
- sends back a
json
response
. - DOES NOT convert
null
andundefined
tojson
.
You need at least one function (you can define more than one) for every request type.
Express
uses middleware
to modify and inspect the incoming request
.
Tons of community made middleware
, for:
- parsing urls,
- handing auth,
- serve static assets,
- ...
A middleware
is just a composable function.
The callback
function that handles the get
request
at the /todos
route
, technically, is middleware
too:
app.get('/todos', (req, res) => { 'I AM MIDDLEWARE, TOO!' });
Create a basic server with express
, that:
- sends back the
index.html
file on aGET
to'/'
- sends back
jsonData
on aGET
to'/data'
First thing to do after cloning a new Node
repo:
âś” npm install
const path = require('path');
app.get('/', (_, res) => {
res.sendFile(path.join(__dirname, 'index.html'));
});
const fs = require('fs');
app.get('/', (_, res) => {
fs.readFile('index.html', (_, buffer) => {
res.setHeader('Content-Type', 'text/html');
res.send(buffer.toString());
});
});
Internally res.sendFile
is leveraging fs.readFile
.
You need to install all these modules per project...
âś” npm install --save-dev babel-cli babel-core babel-eslint babel-preset-es2015 babel-preset-stage-2 eslint eslint-config-airbnb eslint-plugin-babel eslint-plugin-react
...and globally install all these modules to keep Spacemacs
happy (just once per node
version):
âś” npm install -g babel-eslint eslint eslint-config-airbnb eslint-plugin-babel eslint-plugin-react js-beautify tern
Also, remember to copy a .babelrc
file and a eslint.json
file.
.babelrc
{
"presets": ["es2015", "stage-2"]
}
eslint.json
{
"extends": "airbnb",
"env": {
"browser": true,
"node": true,
"mocha": true
},
"ecmaFeatures": {
"forOf": true,
"jsx": true,
"modules": true,
"es6": true
},
"parser": "babel-eslint",
"rules": {
"arrow-body-style": 0,
"comma-dangle": 0,
"indent": [2, 2, {"SwitchCase": 1}],
"new-cap": [2, {
"capIsNewExceptions": [
"Immutable.Map",
"Map",
"Immutable.Set",
"Set",
"Immutable.List",
"List"
]
}],
"no-multi-spaces": [2, {
"exceptions": {
"ImportDeclaration": true,
"VariableDeclarator": true
}
}],
"space-before-function-paren": [2, "always"],
"quote-props": [2, "consistent-as-needed"],
"babel/generator-star-spacing": 1,
"babel/new-cap": 1,
"babel/object-shorthand": 1,
"babel/arrow-parens": 1,
"babel/no-await-in-loop": 1
},
"plugins": [
"babel",
"react"
]
}
- Stateless requests.
- Use HTTP verbs explicitly.
- Expose a directory-like url-pattern for the
routes
(matching resources). - Transfer
JSON
(orXML
).
We are going to model a lion
resource
with name
, id
, age
, pride
and gender
properties.
An example of a lion
resource
in JSON
:
{
"name": "Simba",
"id": 1,
"age": 3,
"pride": "the cool cats",
"gender": "male"
}
Use the HTTP verbs (GET
, POST
, PUT
, DELETE
) to perfom CREATE
, READ
, UPDATE
, DELETE
operations on our resource
.
For a lion
resource
it could be something like this:
"GET /lions"
"GET /lions/:id"
"POST /lions"
"PUT /lions/:id"
"DELETE /lions/:id"
An example of the routes
for a lion
resource
:
{
"GET /lions": {
"desc": "return all lions",
"response": "200 application/json",
"data": [{}, {}, {}]
},
"GET /lions/:id": {
"desc": "return the lion that matches id",
"response": "200 application/json",
"data": {}
},
"POST /lions": {
"desc": "create and return a new lion with the posted data",
"response": "201 application/json",
"data": {}
},
"PUT /lions/:id": {
"desc": "update and return the lion that matches id with the posted update object",
"response": "200 application/json",
"data": {}
},
"DELETE /lions/:id": {
"desc": "delete and return the lion that matches id",
"response": "200 application/json",
"data": {}
}
}
"GET /lions"
app.get('/lions', (_, res) => {
res.json(lions);
});
"GET /lions/:id"
app.get('/lions/:id', (req, res) => {
const lionId = req.params.id;
res.json(
lions.filter((lion) => lion.id === lionId)[0]);
});
"POST /lions"
app.post('/lions', (req, res) => {
const assembleNewLion = () => {
id += 1;
return { ...req.body, id: id.toString(10) };
};
const lion = assembleNewLion();
lions = [...lions, lion];
res.json(lion);
});
"PUT /lions/:id"
app.put('/lions/:id', (req, res) => {
const updateData = req.body;
const lionId = req.params.id;
let updatedLion = undefined;
// In case you try to modify the id
if (updateData.id) { delete updateData.id; }
lions = lions.map((lion) => {
if (lion.id !== lionId) { return lion; }
updatedLion = { ...lion, ...updateData };
return updatedLion;
});
updatedLion
? res.json(updatedLion)
: res.send();
});
"DELETE /lions/:id"
app.delete('/lions/:id', (req, res) => {
const lionId = req.params.id;
let deletedLion = undefined;
lions = lions.map((lion) => {
if (lion.id !== lionId) { return lion; }
deletedLion = lion;
return deletedLion;
});
deletedLion
? res.json(deletedLion)
: res.send();
});
const app = express();
app.use(middleware1);
app.use(middleware2);
app.use(middleware3);
Register middleware
s and execute them in order.
It's a built-in Express
middleware that will serve as a static resource:
- Everything within the passed
dirPath
. index.html
(at the root ofdirPath
) on aGET
to'/'
.
Also sets the MIME
type based on each file's extension.
Example:
const app = express();
app.use(express.static('client'));
Make it possible to send JSON
to the server, that can lately be accessed as req.body
.
Express
by itself doesn't know how to treat JSON
.
import bodyParser from 'body-parser';
const app = express();
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
Validate in the DB (I'm not sure about this).
Middleware
is a function with access to:
- the
request
object - the
response
object - the
next
function (when called will go to the nextmiddleware
)
Middleware
s can:
- run any code
- change the
request
object - change the
response
object - end the
request
-response
cycle - call the next
middleware
in the stack
The server will hang if a middleware
:
- does not call
next()
(invoking the nextmiddleware
in the stack)
OR
- end the
request
-response
cycle
There are 5 different types of middleware
in Express
4.x:
- 3rd party
- Built-in
express.*
- Application level
- Router level
- Error-handling
(err, req, res, next) => {}
- We can apply any
middleware
at theroute
level, like so:
import checkAuth from './util/checkauth';
app.get('/todos', checkAuth(), (req, res) => { .... });
- We can register
middleware
at theapplication
level with.use()
:
import morgan from 'morgan';
app.use(morgan());
This options
middleware
:
const options = (req, res, next) => {
....
next();
};
is registered like this (passing the function as an argument):
app.use(options);
This options
middleware
is a wrapper of the actual middleware
(the inner anonymous function):
const options = (opts) => {
return (req, res, next) => {
....
next();
};
};
and is registered like this (invoking the function as an argument):
app.use(options({ prop: 'whatever' }));
app.param('id', (req, res, next, id) => { .... });
app.param
will run its callback if it detects a query parameter with the given name (in the example :id
).
Kind of a composed router.
import express from 'express';
const app = express();
const lionsRouter = express.Router();
lionsRouter.get('/', (req, res) => { res.json(lions); });
app.use('/lions', lionsRouter);
- The
lionsRouter
is mounted to the'/lions'
url, usingapp.use()
. - The root of
lionsRouter
is not '/
', it's whatever is mounted in it (in this case'/lions'
).
app.use((req, res, next) => {
console.log('The body: ', req.body);
next();
});
- Extract the
App
from the file where you start the server, so theApp
:- can be exported (
export default App
), - can be imported by tests (
import App from './App'
) if necessary.
- can be exported (
- There's a global object in
Node
calledprocess.env
. - We can access our environment variables on this object.
- A useful one is
process.env.NODE_ENV
.
If you have a file by the name of index.js
in a directory, you can just require
that directory and it would automacially give you the index.js
.
âś” npm install --save-dev chai mocha supertest
"scripts": {
"start": "nodemon index.js --exec babel-node",
"lint": "eslint .",
"test": "mocha --compilers js:babel-register ./server"
}
Mocha
as the test runner (describe
,context
,it
).Supertest
managing thehttp
related bits.Chai
as an assertion library.
Different components to support:
- The
API
- Authentication
- Static serving
The API
is a collection of resources
with:
Models
to define how theresources
look.Controllers
to access theresources
.Routes
:- To let the
controllers
know how to run. - To expose our
API
.
- To let the
Instead of grouping our code by type (all controllers
to the controllers
folder, all models
to the models
folder, ...), we are going to group our code by feature
(including tests
).
- api/
- todos/
todoController.js
todoController.specs.js
todoModel.js
todoModel.specs.js
todoRoutes.js
todoRoutes.specs.js
- todos/
- config/
- utils/
index.js
package.json
Use process.env.NODE_ENV
to tell our application in which environment is running in:
development
production
testing
Those 3 are the most common ones, but you can add the ones you want at will.
As a convention, use development
as the default.
- Set and reference other
env
vars, aside fromprocess.env.NODE_ENV
. - Create a central location for the
config
files. - Depending on the
process.env.NODE_ENV
value, we can require differentconfig
files and merge them so our app can use it.
config/config.js
// Base config object
const config = {
dev: 'development',
test: 'testing',
prod: 'production',
port: process.env.PORT || 3000,
env: '',
secrets: {
githubToken: process.env.GITHUB_TOKEN,
jwtSecret: process.env.JWT_SECRET
}
};
process.env.NODE_ENV = process.env.NODE_ENV || config.dev;
config.env = process.env.NODE_ENV;
// Load up development.js || testing.js || production.js
let envConfig = {};
try {
envConfig = require(`./${config.env}`);
} catch (e) {
envConfig = {};
}
// Merge the two objects (using ES2015 Object.assign).
const configToExport = Object.assign({}, config, envConfig);
// Export the resulting object so our App can use it.
module.exports = configToExport;
The secrets
are not hardcoded (jwtSecret: process.env.JWT_SECRET
) so the config.js
file can go into source control.
Think about:
- What
resources
we would need. - The
routes
for thatresources
. - Don't immediately start modelling the
resources
.
- Relative paths for
imports
. - Absolute paths for anything related to file system.
Mongo
is aNoSQL
Document Store.Mongo
does not care about the form of our data:- Don't have to model the data.
- Can just throw
json
in it and ask for it later.
âś” mongod --config /usr/local/etc/mongod.conf
âś” mongo
âś” mongo
MongoDB shell version: 3.2.3
connecting to: test
> show dbs
flicks 0.078GB
grocery 0.078GB
local 0.078GB
> use puppies
switched to db puppies
> db.createCollection('toys')
{ "ok" : 1 }
> show collections
system.indexes
toys
> db.toys.insert({ name: 'yoyo', color: 'red' })
WriteResult({ "nInserted" : 1 })
> db.toys.find()
{ "_id" : ObjectId("56d85e335b44363b1a940cef"), "name" : "yoyo", "color" : "red" }
âś” npm install --save mongoose
import mongoose from 'mongoose';
mongoose.connect('mongodb://localhost/nameOfTheDBYouWant');
- We can use
schemas
inmongoose
to add some structure and validations to our data. Mongo
does not needschemas
.
import mongoose from 'mongoose';
const TodoSchema = new mongoose.Schema({
completed: Boolean,
// Add validations by using an object literal
content: {
type: String,
required: true
}
});
// Add a 'todos' Collection using our TodoSchema
const TodoModel = mongoose.model('Todo', TodoSchema);
export default TodoModel;
const DogSchema = new mongoose.Schema({
name: {
type: String,
required: true,
unique: true
},
whenAdopted: Date,
hasShots: Boolean,
collarCode: Buffer,
age: {
type: Number,
min: 0,
max: 30
},
toys: [],
location: {
state: String,
city: String,
zip: Number
},
// Relationship example: "A dog belongs to an owner".
owner: {
// the owner's id
type: mongoose.Schema.Types.ObjectId,
// there's an owner schema, somewhere
ref: 'owner',
required: true
}
});
Schema
: The blueprint for the data (how I want my data to look like).Model
:- The
javascript
representation of the data we are going to use. - Allow us to access the
document
in thedatabase
.
- The
Document
: The data representation inside thedatabase
(belongs to theCollection
).Collection
: A group ofdocument
s inside thedatabase
.
The model
and the document
are the same thing, but the model
lives in our javascript
code while the document
lives in mongo
.
- Normalized data:
- Only one way to access a concrete piece of data.
- Example: Posts know about authors, but authors don't know about posts.
- Denormalized data:
- More than one way to access a concrete piece of data.
- Example: Posts know about authors, and authors know about posts.
Mongo
has a sophisticated query syntax that is full of options.
import Post from './postModel';
postRouter.route('/')
.get((req, res, next) => {
Post.find(
{ title: 'My awesome blog post' },
(err, docs) => err ? next(err) : res.json(docs)
);
});
- Helpful for
GET
,PUT
andDELETE
, since an:id
is needed.
import Post from './postModel';
postRouter.route('/')
.get((req, res, next) => {
Post.findById(
'56d85e335b44363b1a940cef',
(err, doc) => err ? next(err) : res.json(doc)
);
});
- GOTCHA: It will blow away the properties of the document than are not handled to it. So, you need to handle the updated properties and also the non-updated properties.
import Post from './postModel';
postRouter.route('/')
.put((req, res, next) => {
Post.findByIdAndUpdate(
'56d85e335b44363b1a940cef',
// SAY BYE BYE to the other properties aside from title
{ title: 'My updated title' },
(err, updatedDoc) => err ? next(err) : res.json(updatedDoc)
);
});
import Post from './postModel';
postRouter.route('/')
.post((req, res, next) => {
const post = new Post({
title: 'My awesome blog post',
....
});
post.save(
(err, savedDoc) => err ? next(err) : res.json(savedDoc)
);
});
import Post from './postModel';
postRouter.route('/')
.post((req, res, next) => {
const post = new Post();
post.title: 'My awesome blog post';
....
post.save(
(err, savedDoc) => err ? next(err) : res.json(savedDoc)
);
});
import Post from './postModel';
postRouter.route('/')
.post((req, res, next) => {
const post = {
title: 'My awesome blog post',
....
};
Post.create(
post,
(err, savedDoc) => err ? next(err) : res.json(savedDoc)
);
});
import Post from './postModel';
postRouter.route('/')
.delete((req, res, next) => {
....
doc.remove(
(err, removedDoc) => err ? next(err) : res.json(removedDoc)
);
});
- Since
Mongo
is aNoSQL
Database, there's no join tables. Population
s are:- a join-table-like-at-call-time solution.
- a way to hydrate
model
's relationships at call time.
// THE SCHEMAS
const DogSchema = new mongoose.Schema({
owner: {
type: mongoose.Schema.Types.ObjectId,
ref: 'person'
},
name: String
});
const PersonSchema = new mongoose.Schema({
name: String
});
// THE MODELS
const Dog = mongoose.model('dog', DogSchema);
const Person = mongoose.model('person', PersonSchema);
// THE POPULATION:
// - find all dogs `.find({})`
// - populate their owners `.populate('owner')`
// - do it! `.exec()`
// This will actually:
// - grab the owners object
const getAllDogs = (req, res, next) => {
Dog
.find({})
.populate('owner')
.exec()
.then(
(dogs) => res.json(dogs),
(err) => next(err)
);
};
- Does
mongoose
promise
library follow theA+
promise
s specification? - Wo we need to
Promisify
mongoose
withBlueBird
?
NOTE: Taken from Built-in Promises.
You can plug in your own Promise
s Library since Mongoose
4.1.0
Just set mongoose.Promise
to your favorite ES6-style promise
constructor and mongoose
will use it.
const query = Band.findOne({name: "Guns N' Roses"});
// Use ES2015 native promises
mongoose.Promise = global.Promise;
assert.equal(query.exec().constructor, global.Promise);
// Use bluebird
mongoose.Promise = require('bluebird');
assert.equal(query.exec().constructor, require('bluebird'));
// Use q. Note that you **must** use `require('q').Promise`.
mongoose.Promise = require('q').Promise;
assert.ok(query.exec() instanceof require('q').makePromise);
const action = (cb) => {
setTimeout(() => cb('hey'), 2000);
};
action(
(arg) => console.log(arg)
);
A Promise is just an Object with a couple of functions.
const action = () =>
new Promise(
(resolve, reject) => setTimeout(() => resolve('hey'), 2000)
);
action()
.then((word) => console.log(word))
.catch((err) => console.log(err));
import fs from 'fs';
const readFile = () =>
new Promise((resolve, reject) =>
fs.readFile(
'./package.json',
(err, file) => err ? reject(err) : resolve(file.toString())
)
);
readFile()
.then((file) => console.log(file))
.catch((err) => console.log(err));
Whatever you return
ed inside the then()
callback, is then wrapped into another promise (monad?).
A typical chaining pattern:
readFile()
.then(logFile)
.then(sendEmail)
.then(callCustomer)
.catch(handleErrors);
const readAllFiles = () => {
const promises = [promise1, promise2, promise3];
return Promise.all(promises);
};
readAllFiles()
.then((files) => files.map(f => console.log(f)))
.catch((err) => console.log(err));
- Generators
- Fibers
- Async/Await
JWT
is a heavily used open standard.- Because we're using a
token
approach, we don't need to keep track of who is signed in with asession store
or havecookies
. Cookies
don't work very well in mobile environments (it's not as easy as on the Web).- The
JWT
will be sent on everyrequest
(REST
is stateless). - The
JWT
has to be stored on the client that is requesting resources (usually in thelocal storage
). JWT
s can be used withOAuth
, too.
const user = {_id: '2873273237328378273'};
// Generate a JWT based on user's id
const token = jwt.sign(user, 'shhhh, its a secret');
// Send the JWT back to the client on signup/signin
//...
// Later on we have an incoming request, so:
// - we decode the JWT to see who the user is.
// - the JWT is probably on the authorization header.
// - throw an error if the JWT is not a valid JWT and instead is a random string.
const user = jwt.verify(req.headers.authorization, 'shhhh, its a secret');
// If it's a valid JWT, we could then proceed to look the user up to see if she exists in our system
User.findById(user._id, () => {});
jwt.sign()
as little as you need (enough to identify who is making therequest
).- Don't
jwt.sign()
an entireuser
if it's not strictly necessary.
- A user signs up to access protected resources on our api, (
username
andpassword
). - On success, we create a new user in our database.
- We use the new user's id to issue a
JWT
. - We send that
JWT
back to the user on the signup's response, so she can: 5. save it 6. send it back on every request to a protected resource. - We get authentication
- We also get identification, because we can reverse the
JWT
to be its original object and get the user's id.
- DO NOT store plain text passwords in your database, store a
hash
ed andsalt
ed version. - DO NOT send plain text passwords over
HTTP
, useHTTPS
(useHTTPS
for your whole site, if you can). - To further prevent rainbow attacks create unique
salt
s for each user and store thesalt
on the user. salt
andhash
asynchronoulsy to avoid time attacks.
There are so many ways to implement passwords with express
and mongo
.
We are going to use mongoose
middleware for that task.
// METHODS
// - almost like 'prototype'.
// - available to this concrete instance:
// * not other instances
// * not the Dog itself
DogSchema.methods.bark = () => {
// this === the dog document
};
// STATICS
// - almost like static methods.
// - available to the Dog itself:
// * not instances of Dog
DogSchema.statics.findByOwner = () => {
// this === the Dog Constructor
};
mongoose
middleware
will attach themselves to life cycle events around document
s
// `.post('save')` ===> After `save`
DogSchema.post('save', (next) => { .... });
// `.pre('validate')` ===> Before `validate`
DogSchema.pre('validate', (next) => { .... });
- In non very private resources, at least
POST
,PUT
andDELETE
should be protected. - In very private resources, protect everything.
api/users/me
route:
- Very common with JWT.
- Used for a user that has the JWT.
Browsers, by default, are not going to grant access to a route in localhost:3000
if we are on localhost:4500
.
XMLHttpRequest cannot load http://localhost:3000/api/posts.
No 'Access-Control-Allow-Origin' header is present on the requested resource.
Origin 'http://localhost:4500' is therefore not allowed access.
To avoid this behaviour:
- Enable CORS in your server (share resources across different origins).
An API that other people is going to consume, should be CORS
enable.
If you have a proxy like Ngnix
you should deal with CORS
there.
A browser asks the server (using the OPTIONS
http verb), am I allowed to request from you?
- the server responds with a 2xx allowing requests (then
CORS
is enabled). - the server responds with a denying requests (then
CORS
is disabled)
A great way to eensure that we don't see a hashed password in your data.
Delete password
from user
:
UserSchema.methods = {
....
toJson () {
const obj = this.toObject;
delete obj.password;
return obj;
}
};
Get only username
from author
:
Post
.findById(postId)
.populate('author', 'username')
.exec()
Do not get password
from user
:
User
.findById(userId)
.select('-password')
JWT
s are for the users (authentication).- API keys are for authorizing clients (iOS, android, web, ...) to use an API.
You could probably use a JWT
as an API Key.
- Use
envs
for secrets (don't check them intogithub
) - Don't hardcode dev urls, db urls, ports, etc
- Make sure you are using error handling
- Make sure all your dependencies are being installed
- If you're going to have your platform build for you, make sure it has access to all your build tools (
grunt
,gulp
,webpack
) - You can freeze
node modules
by usingnpm shrinkwrap
web: node index.js
Run it like heroku
will run it:
foreman start
- You should probably save also the slug on the DB (on the Model)
NOTE TO SELF: Check my notes of RESTful Rails Development book about this.
Investigate how to use Redis
if you want to use sessions when authenticating