writing restful web services using node.js
DESCRIPTION
by Jakob Mattsson on Frontend DEV Conf'13 http://bit.ly/Jakob_MattssonTRANSCRIPT
@jakobmattsson
Started out doing consulting
3 startups:RecruitingAdvertisingFeedback
2 000 000 000 writes/day!
Back to square one
Writing RESTfulweb servicesusing Node.js
Comparison
Rocket science
Product demo
Silver bullet
Comparison
Rocket science
Product demo
Silver bulletNOT
What is it then?
Imagination
Quantity
Bottom up
Principles
Also... CoffeeScript
Node.js
Node.js is a platform built on Chrome's JavaScript runtime for easily building fast,
scalable network applications.
Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient,
perfect for data-intensive real-time applications that run across distributed devices.
Node.js is a platform built on Chrome's JavaScript runtime for easily building fast,
scalable network applications.
Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient,
perfect for data-intensive real-time applications that run across distributed devices.
fs = require('fs')
fs.readFile 'meaning_of_life.txt', 'utf-8', (err, data) -> console.log(data)
console.log('end')
end42
Several protocols,including TCP and HTTP,
are built in to node.
http = require('http')
onRequest = (req, res) -> res.writeHead(200, { 'Content-Type': 'text/plain' }) res.end('Hello World\n')
http.createServer(onRequest).listen(1337)
npm
npm is a package manager for node.
You can use it to install and publish your node programs.
”It manages dependencies and does other cool stuff.”
npm install underscore
_ = require('underscore')numbers = _.range(1, 10)console.log(_.last(numbers))
Connect
Connect is a midleware framework for node.
It’s shipping with over 18 bundled middleware.
It has a rich selection of 3rd-party middleware.
npm install connect
connect = require('connect')app = connect()app.listen(3000)
// last line equivalent to // http.createServer(app).listen(3000);
connect = require('connect')app = connect()
app.use connect.basicAuth (user, pass) -> return user == 'jakob' && pass == 'fdc13'
app.use (req, res) -> res.writeHead(200, { 'Content-Type': 'text/plain' }) res.end('Hello World\n')
app.listen(3000)
logger csrf
compress basicAuth
bodyParser json
urlencoded multipart
cookieParser session
cookieSession methodOverride
responseTime staticCache
static directory
vhost favicon
limit query
errorHandler
Request logger with custom format supportCross-site request forgery protectionGzip compression middlewareBasic http authenticationExtensible request body parserApplication/json parserApplication/x-www-form-urlencoded parserMultipart/form-data parserCookie parserSession management support with bundled MemoryStoreCookie-based session supportFaux HTTP method supportCalculates response-time and exposes via X-Response-TimeMemory cache layer for the static() middlewareStreaming static file server supporting Range and moreDirectory listing middlewareVirtual host sub-domain mapping middlewareEfficient favicon server (with default icon)Limit the bytesize of request bodiesAutomatic querystring parser, populating req.queryFlexible error handler
Express
High performancehigh class web development
for Node.js
npm install express
express = require('express')app = express.createServer()
app.get '/', (req, res) -> res.send('Hello World')
app.get '/users/:id', (req, res) -> res.send('user ' + req.params.id)
app.listen(3000)
express = require('express')app = express.createServer()
before1 = (req, res, next) -> req.foo = 'bar' next()
before2 = (req, res, next) -> res.header('X-Time', new Date().getTime()) next()
app.get '/', before1, (req, res) -> res.send('Hello World')
app.get '/users/:id', [before1, before2], (req, res) -> console.log(req.foo) res.send('user ' + req.params.id)
app.listen(3000)
Data storage
But which one?
Schemaless is a lie
Mongoose
Mongoose is a MongoDB object modeling tool
designed to work in an asynchronous environment.
npm install mongoose
mongoose = require 'mongoose'
mongoose.connect 'mongodb://localhost/tamblr'
model = (name, schema) -> mongoose.model name, new mongoose.Schema schema, strict: true
users = model 'users' name: type: String default: '' bio: type: String default: 'IE6-lover' age: type: Number default: null
blogs = model 'blogs' name: type: String default: '' description: type: String default: '' users: type: ObjectId ref: 'users'
posts = model 'posts' title: type: String default: '' body: type: String default: '' published: type: Date blogs: type: ObjectId ref: 'blogs'
list = (model, callback) -> model.find {}, callback
get = (model, id, callback) -> model.findById id, callback
del = (model, id, callback) -> model.remove { _id: id }, callback
put = (model, id, data, callback) -> model.update { _id: id }, data, { multi: false }, callback
post = (model, data, callback) -> new model(data).save callback
app.get '/users/:id', (req, res) -> get users, req.params.id, (err, data) -> res.json data
copy-paste!
POST /usersGET /usersGET /users/42DELETE /users/42PUT /users/42
POST /blogsGET /blogsGET /blogs/42DELETE /blogs/42PUT /blogs/42
POST /postsGET /postsGET /posts/42DELETE /posts/42PUT /posts/42
or should we?
models = [users, blogs, posts]
Object.keys(models).forEach (modelName) ->
app.get "/#{modelName}", (req, res) -> list models[modelName], (err, data) -> res.json data
app.get "/#{modelName}/:id", (req, res) -> get models[modelName], req.params.id, (err, data) -> res.json data
app.post "/#{modelName}", (req, res) -> post models[modelName], req.body, (err, data) -> res.json data
app.del "/#{modelName}/:id", (req, res) -> del models[modelName], req.parmas.id, (err, count) -> res.json { count: count }
app.put "/#{modelName}/:id", (req, res) -> put models[modelName], req.params.id, req.body, (err, count) -> res.json { count: count }
POST /usersGET /usersGET /users/42DELETE /users/42PUT /users/42
POST /blogsGET /blogsGET /blogs/42DELETE /blogs/42PUT /blogs/42
POST /postsGET /postsGET /posts/42DELETE /posts/42PUT /posts/42
But what about the relations/associations?
POST /users/42/blogsGET /users/42/blogs
POST /blogs/42/postsGET /blogs/42/posts
paths = models[modelName].schema.pathsowners = Object.keys(paths).filter (p) -> paths[p].options.type == ObjectId && typeof paths[p].options.ref == 'string'.map (x) -> paths[x].options.ref
owners.forEach (owner) ->
app.get "/#{owner}/:id/#{name}", (req, res) -> listSub models[name], owner, req.params.id, (err, data) -> res.json data
app.post "/#{owner}/:id/#{name}", (req, res) -> postSub models[name], req.body, owner, req.params.id, (err, data) -> res.json data
POST /users/42/blogsGET /users/42/blogs
POST /blogs/42/postsGET /blogs/42/posts
Keep on generating!
Authentication
npm install passportnpm install passport-local
passport = require('passport')passportLocal = require('passport-local')
passport.use new passportLocal.Strategy (user, pass, done) -> findUserPlz { username: user, password: pass }, (err, user) -> done(err, user)
app.use(passport.initialize())app.use(passport.authenticate('local'))
npm install passport-twitter
passport = require('passport')twitter = require('passport-twitter')
keys = { consumerKey: TWITTER_CONSUMER_KEY consumerSecret: TWITTER_CONSUMER_SECRET callbackURL: "http://127.0.0.1:3000/auth/twitter/callback"}
passport.use new twitter.Strategy keys, (t, ts, profile, done) -> findOrCreateUserPlz { twitterId: profile.id }, (err, user) -> done(err, user)
app.use(passport.initialize())app.use(passport.authenticate('twitter'))
Part 2Convention
ALL CHARACTERS ANDEVENTS IN THIS SHOW--
EVENT THOSE BASED ON REALPEOPLE--ARE ENTIRELY FICTIONAL.
ALL CELEBERTY VOICES AREIMPERSONATED.....POORLY. THE
FOLLOWING PROGRAM CONTAINSCOARSE LANGUAGE AND DUE TOITS CONTENT IT SHOULD NOT BE
VIEWED BE ANYONE
Verbs vs Nouns
/users/users/42
/blogs/blogs/73
/posts/posts/314
GET /usersGET /users/42
POST /users
PUT /users/42
DELETE /usersDELETE /users/42
Associations
/users/blogs/posts
/users/blogs/posts
/users/42/blogs/blogs/314/posts
/users/blogs/posts
/users/42/blogs/blogs/314/posts
/users/42/blogs/314/posts
/users/blogs/posts
/users/42/blogs/blogs/314/posts
/users/42/blogs/314/posts
/users/blogs/posts
/users/42/blogs/blogs/314/posts
/users/42/blogs/314/posts
Keep URLs short. Don’t overqualify.
GET /blogs/42/posts?tag=javascript
Versions
GET /v1/users
Partial responses
GET /users?fields=email,age
GET /users?limit=10&offset=0
Verbs
/convert?from=EUR&to=BYR&amount=100
Content-types
GET /v1/users/42.xml
Attributes
{ "userId": 1, "firstName": "Jakob", "lastName": "Mattsson"}
Search
GET /search?q=javascriptGET /blog/42/posts?q=javascript
Authentication
Part 3Conclusion
It goes for ideas too!
Reuse convention.
Reuse code.
Resuse ideas.
Build new things.
”MOST IMPORTANT STEPFOR BUILD PRODUCTIS BUILD PRODUCT”
- @fakegrimlock
@jakobmattsson
Thank you!