Skip to content

Instantly share code, notes, and snippets.

@pixelhandler
Created February 10, 2012 17:24
Star You must be signed in to star a gist
Save pixelhandler/1791080 to your computer and use it in GitHub Desktop.
Develop a RESTful API Using Node.js With Express and Mongoose - See: http://pixelhandler.com/blog/2012/02/09/develop-a-restful-api-using-node-js-with-express-and-mongoose/
var application_root = __dirname,
express = require("express"),
path = require("path"),
mongoose = require('mongoose');
var app = express.createServer();
// database
mongoose.connect('mongodb://localhost/ecomm_database');
// config
app.configure(function () {
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(path.join(application_root, "public")));
app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
});
var Schema = mongoose.Schema; //Schema.ObjectId
// Schemas
var Sizes = new Schema({
size: { type: String, required: true },
available: { type: Number, required: true, min: 0, max: 1000 },
sku: {
type: String,
required: true,
validate: [/[a-zA-Z0-9]/, 'Product sku should only have letters and numbers']
},
price: { type: Number, required: true, min: 0 }
});
var Images = new Schema({
kind: {
type: String,
enum: ['thumbnail', 'catalog', 'detail', 'zoom'],
required: true
},
url: { type: String, required: true }
});
var Variants = new Schema({
color: String,
images: [Images],
sizes: [Sizes]
});
var Categories = new Schema({
name: String
});
var Catalogs = new Schema({
name: String
});
// Product Model
var Product = new Schema({
title: { type: String, required: true },
description: { type: String, required: true },
style: { type: String, unique: true },
images: [Images],
categories: [Categories],
catalogs: [Catalogs],
variants: [Variants],
modified: { type: Date, default: Date.now }
});
// validation
Product.path('title').validate(function (v) {
console.log("validate title");
console.log(v);
return v.length > 10 && v.length < 70;
});
Product.path('style').validate(function (v) {
console.log("validate style");
console.log(v);
return v.length < 40;
}, 'Product style attribute is should be less than 40 characters');
Product.path('description').validate(function (v) {
console.log("validate description");
console.log(v);
return v.length > 10;
}, 'Product description should be more than 10 characters');
var ProductModel = mongoose.model('Product', Product);
/* Product Document
[
{
"title": "My Awesome T-shirt",
"description": "All about the details. Of course it's black.",
"images": [
{
"kind": "thumbnail",
"url": "images/products/1234/main.jpg"
}
],
"categories": [
{ "name": "Clothes" },
{ "name": "Shirts" }
],
"style": "1234",
"variants": [
{
"color": "Black",
"images": [
{
"kind": "thumbnail",
"url": "images/products/1234/thumbnail.jpg"
},
{
"kind": "catalog",
"url": "images/products/1234/black.jpg"
}
],
"sizes": [
{
"size": "S",
"available": 10,
"sku": "CAT-1234-Blk-S",
"price": 99.99
},
{
"size": "M",
"available": 7,
"sku": "CAT-1234-Blk-M",
"price": 109.99
}
]
}
],
"catalogs": [
{ "name": "Apparel" }
]
}
]
*/
// REST api
app.get('/api', function (req, res) {
res.send('Ecomm API is running');
});
// POST to CREATE
app.post('/api/products', function (req, res) {
var product;
console.log("POST: ");
console.log(req.body);
product = new ProductModel({
title: req.body.title,
description: req.body.description,
style: req.body.style,
images: req.body.images,
categories: req.body.categories,
catalogs: req.body.catalogs,
variants: req.body.variants
});
product.save(function (err) {
if (!err) {
return console.log("created");
} else {
return console.log(err);
}
});
return res.send(product);
});
// PUT to UPDATE
// Bulk update
app.put('/api/products', function (req, res) {
var i, len = 0;
console.log("is Array req.body.products");
console.log(Array.isArray(req.body.products));
console.log("PUT: (products)");
console.log(req.body.products);
if (Array.isArray(req.body.products)) {
len = req.body.products.length;
}
for (i = 0; i < len; i++) {
console.log("UPDATE product by id:");
for (var id in req.body.products[i]) {
console.log(id);
}
ProductModel.update({ "_id": id }, req.body.products[i][id], function (err, numAffected) {
if (err) {
console.log("Error on update");
console.log(err);
} else {
console.log("updated num: " + numAffected);
}
});
}
return res.send(req.body.products);
});
// Single update
app.put('/api/products/:id', function (req, res) {
return ProductModel.findById(req.params.id, function (err, product) {
product.title = req.body.title;
product.description = req.body.description;
product.style = req.body.style;
product.images = req.body.images;
product.categories = req.body.categories;
product.catalogs = req.body.catalogs;
product.variants = req.body.variants;
return product.save(function (err) {
if (!err) {
console.log("updated");
} else {
console.log(err);
}
return res.send(product);
});
});
});
// GET to READ
// List products
app.get('/api/products', function (req, res) {
return ProductModel.find(function (err, products) {
if (!err) {
return res.send(products);
} else {
return console.log(err);
}
});
});
// Single product
app.get('/api/products/:id', function (req, res) {
return ProductModel.findById(req.params.id, function (err, product) {
if (!err) {
return res.send(product);
} else {
return console.log(err);
}
});
});
// DELETE to DESTROY
// Bulk destroy all products
app.delete('/api/products', function (req, res) {
ProductModel.remove(function (err) {
if (!err) {
console.log("removed");
return res.send('');
} else {
console.log(err);
}
});
});
// remove a single product
app.delete('/api/products/:id', function (req, res) {
return ProductModel.findById(req.params.id, function (err, product) {
return product.remove(function (err) {
if (!err) {
console.log("removed");
return res.send('');
} else {
console.log(err);
}
});
});
});
// launch server
app.listen(4242);
jQuery.get("/api/products", function(data, textStatus, jqXHR) {
var ids = [];
console.log("product ids:");
$.each(data, function () {
ids.push(this._id);
});
console.dir(ids);
// 0: "4f3cc9189760db17520000e6"
// 1: "4f3cc91f9760db17520000f0"
// 2: "4f3cc9279760db17520000fb"
// 3: "4f3cc92f9760db1752000105"
});
/*
boilerplate for bulk update, replace null with product object
jQuery.ajax({
url: "/api/products",
type: "PUT",
data: {
"products": [
{
"4f3cc9189760db17520000e6" : null
},
{
"4f3cc91f9760db17520000f0" : null
},
{
"4f3cc9279760db17520000fb" : null
},
{
"4f3cc92f9760db1752000105" : null
},
]
},
success: function(data, textStatus, jqXHR) {
console.log("PUT resposne:");
console.dir(data);
console.log(textStatus);
console.dir(jqXHR);
}
});
*/
jQuery.ajax({
url: "/api/products",
type: "PUT",
data: {
"products": [
{
"4f3cc9189760db17520000e6" : {
"title": "My Awesome T-shirt - UPDATED",
"description": "All about the details. Of course it's black.",
"images": [
{
"kind": "thumbnail",
"url": "images/products/1234/main.jpg"
}
],
"categories": [
{ "name": "Clothes" },
{ "name": "Shirts" }
],
"style": "1234",
"variants": [
{
"color": "Black",
"images": [
{
"kind": "thumbnail",
"url": "images/products/1234/thumbnail.jpg"
},
{
"kind": "catalog",
"url": "images/products/1234/black.jpg"
}
],
"sizes": [
{
"size": "S",
"available": 10,
"sku": "CAT-1234-Blk-S",
"price": 29.99
},
{
"size": "M",
"available": 7,
"sku": "CAT-1234-Blk-M",
"price": 19.99
}
]
}
],
"catalogs": [
{ "name": "Apparel" }
]
}
},
{
"4f3cc91f9760db17520000f0" : {
"title": "My Other T-shirt - UPDATED",
"description": "All about the details. Almost as nice as my Awesome T-Shirt",
"images": [
{
"kind": "thumbnail",
"url": "images/products/1235/main.jpg"
}
],
"categories": [
{ "name": "Clothes" },
{ "name": "Shirts" }
],
"style": "1235",
"variants": [
{
"color": "Blue",
"images": [
{
"kind": "thumbnail",
"url": "images/products/1235/thumbnail.jpg"
},
{
"kind": "catalog",
"url": "images/products/1235/blue.jpg"
}
],
"sizes": [
{
"size": "S",
"available": 8,
"sku": "CAT-1235-Blu-S",
"price": 19.99
},
{
"size": "M",
"available": 9,
"sku": "CAT-1235-Blu-M",
"price": 29.99
},
{
"size": "L",
"available": 12,
"sku": "CAT-1235-Blu-L",
"price": 39.99
}
]
}
],
"catalogs": [
{ "name": "Apparel" }
]
}
},
{
"4f3cc9279760db17520000fb" : {
"title": "My Gray T-shirt - UPDATED",
"description": "All about the details. Not too much color here, just gray.",
"images": [
{
"kind": "thumbnail",
"url": "images/products/1236/main.jpg"
}
],
"categories": [
{ "name": "Clothes" },
{ "name": "Shirts" }
],
"style": "1236",
"variants": [
{
"color": "Gray",
"images": [
{
"kind": "thumbnail",
"url": "images/products/1236/thumbnail.jpg"
},
{
"kind": "catalog",
"url": "images/products/1236/gray.jpg"
}
],
"sizes": [
{
"size": "S",
"available": 25,
"sku": "CAT-1236-Gra-S",
"price": 14.99
},
{
"size": "L",
"available": 16,
"sku": "CAT-1236-Gra-L",
"price": 24.99
}
]
}
],
"catalogs": [
{ "name": "Apparel" }
]
}
},
{
"4f3cc92f9760db1752000105" : {
"title": "My Red Hot T-shirt - UPDATED",
"description": "All about the details. Red Hot T, get 'em while they're hot.",
"images": [
{
"kind": "thumbnail",
"url": "images/products/1237/main.jpg"
}
],
"categories": [
{ "name": "Clothes" },
{ "name": "Shirts" }
],
"style": "1237",
"variants": [
{
"color": "Red",
"images": [
{
"kind": "thumbnail",
"url": "images/products/1237/thumbnails/red.jpg"
},
{
"kind": "catalog",
"url": "images/products/1237/red.jpg"
}
],
"sizes": [
{
"size": "S",
"available": 25,
"sku": "CAT-1237-Red-S",
"price": 19.99
},
{
"size": "L",
"available": 16,
"sku": "CAT-1237-Red-L",
"price": 29.99
}
]
},
{
"color": "White",
"images": [
{
"kind": "thumbnail",
"url": "images/products/1237/thumbnails/white.jpg"
},
{
"kind": "catalog",
"url": "images/products/1237/white.jpg"
}
],
"sizes": [
{
"size": "M",
"available": 7,
"sku": "CAT-1237-Whi-M",
"price": 9.99
},
{
"size": "L",
"available": 8,
"sku": "CAT-1237-Whi-L",
"price": 21.99
}
]
}
],
"catalogs": [
{ "name": "Apparel" }
]
}
}
]
},
success: function(data, textStatus, jqXHR) {
console.log("PUT resposne:");
console.dir(data);
console.log(textStatus);
console.dir(jqXHR);
}
});
jQuery.post("/api/products", {
"title": "My Awesome T-shirt",
"description": "All about the details. Of course it's black.",
"images": [
{
"kind": "thumbnail",
"url": "images/products/1234/main.jpg"
}
],
"categories": [
{ "name": "Clothes" },
{ "name": "Shirts" }
],
"style": "1234",
"variants": [
{
"color": "Black",
"images": [
{
"kind": "thumbnail",
"url": "images/products/1234/thumbnail.jpg"
},
{
"kind": "catalog",
"url": "images/products/1234/black.jpg"
}
],
"sizes": [
{
"size": "S",
"available": 10,
"sku": "CAT-1234-Blk-S",
"price": 99.99
},
{
"size": "M",
"available": 7,
"sku": "CAT-1234-Blk-M",
"price": 109.99
}
]
}
],
"catalogs": [
{ "name": "Apparel" }
]
}, function(data, textStatus, jqXHR) {
console.log("Post resposne:"); console.dir(data); console.log(textStatus); console.dir(jqXHR);
});
// ---
jQuery.post("/api/products", {
"title": "My Other T-shirt",
"description": "All about the details. Almost as nice as my Awesome T-Shirt",
"images": [
{
"kind": "thumbnail",
"url": "images/products/1235/main.jpg"
}
],
"categories": [
{ "name": "Clothes" },
{ "name": "Shirts" }
],
"style": "1235",
"variants": [
{
"color": "Blue",
"images": [
{
"kind": "thumbnail",
"url": "images/products/1235/thumbnail.jpg"
},
{
"kind": "catalog",
"url": "images/products/1235/blue.jpg"
}
],
"sizes": [
{
"size": "S",
"available": 8,
"sku": "CAT-1235-Blu-S",
"price": 79.99
},
{
"size": "M",
"available": 9,
"sku": "CAT-1235-Blu-M",
"price": 89.99
},
{
"size": "L",
"available": 12,
"sku": "CAT-1235-Blu-L",
"price": 99.99
}
]
}
],
"catalogs": [
{ "name": "Apparel" }
]
}, function(data, textStatus, jqXHR) {
console.log("Post resposne:"); console.dir(data); console.log(textStatus); console.dir(jqXHR);
});
// ---
jQuery.post("/api/products", {
"title": "My Gray T-shirt",
"description": "All about the details. Not too much color here, just gray.",
"images": [
{
"kind": "thumbnail",
"url": "images/products/1236/main.jpg"
}
],
"categories": [
{ "name": "Clothes" },
{ "name": "Shirts" }
],
"style": "1236",
"variants": [
{
"color": "Gray",
"images": [
{
"kind": "thumbnail",
"url": "images/products/1236/thumbnail.jpg"
},
{
"kind": "catalog",
"url": "images/products/1236/gray.jpg"
}
],
"sizes": [
{
"size": "S",
"available": 25,
"sku": "CAT-1236-Gra-S",
"price": 19.99
},
{
"size": "L",
"available": 16,
"sku": "CAT-1236-Gra-L",
"price": 29.99
}
]
}
],
"catalogs": [
{ "name": "Apparel" }
]
}, function(data, textStatus, jqXHR) {
console.log("Post resposne:"); console.dir(data); console.log(textStatus); console.dir(jqXHR);
});
// ---
jQuery.post("/api/products", {
"title": "My Red Hot T-shirt",
"description": "All about the details. Red Hot T, get 'em while they're hot.",
"images": [
{
"kind": "thumbnail",
"url": "images/products/1237/main.jpg"
}
],
"categories": [
{ "name": "Clothes" },
{ "name": "Shirts" }
],
"style": "1237",
"variants": [
{
"color": "Red",
"images": [
{
"kind": "thumbnail",
"url": "images/products/1237/thumbnails/red.jpg"
},
{
"kind": "catalog",
"url": "images/products/1237/red.jpg"
}
],
"sizes": [
{
"size": "S",
"available": 25,
"sku": "CAT-1237-Red-S",
"price": 19.99
},
{
"size": "L",
"available": 16,
"sku": "CAT-1237-Red-L",
"price": 29.99
}
]
},
{
"color": "White",
"images": [
{
"kind": "thumbnail",
"url": "images/products/1237/thumbnails/white.jpg"
},
{
"kind": "catalog",
"url": "images/products/1237/white.jpg"
}
],
"sizes": [
{
"size": "M",
"available": 7,
"sku": "CAT-1237-Whi-M",
"price": 18.99
},
{
"size": "L",
"available": 8,
"sku": "CAT-1237-Whi-L",
"price": 27.99
}
]
}
],
"catalogs": [
{ "name": "Apparel" }
]
}, function(data, textStatus, jqXHR) {
console.log("Post resposne:"); console.dir(data); console.log(textStatus); console.dir(jqXHR);
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Frontend - Backbone.js integration with API</title>
</head>
<body>
<section id="api">
<h1>Nouns...</h1>
<p>
/products<br>
/products/:id
</p>
</section>
<section id="container">
<h1>Product List</h1>
<p>
To render the product list use the route '#list' following the url for this page
</p>
</section> <!-- /container -->
<script type="text/template" id="product-template">
<p>{{title}}</p>
<p>{{description}}</p>
</script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.3.1/underscore-min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/backbone.js/0.9.0/backbone-min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/mustache.js/0.3.0/mustache.min.js"></script>
<script>
PX = window.PX || {};
// model
PX.Product = Backbone.Model.extend({
defaults: {
title: null,
description: null
}
});
// collection
(function () {
var ProductList;
ProductList = Backbone.Collection.extend({
model: PX.Product,
url: '/api/products',
initialize: function () {
this.fetch({
success: this.fetchSuccess,
error: this.fetchError
});
this.deferred = new $.Deferred();
},
deferred: Function.constructor.prototype,
fetchSuccess: function (collection, response) {
collection.deferred.resolve();
},
fetchError: function (collection, response) {
throw new Error("Products fetch did get collection from API");
}
});
PX.products = new ProductList();
ProductList = null;
}());
PX.ProductView = Backbone.View.extend({
tagName: "li",
className: "product",
initialize: function (options) {
this.template = $('#product-template').html();
},
render: function () {
var markup = Mustache.to_html(this.template, this.model.toJSON());
this.$el.html(markup).attr('id',this.model.get('_id'));
return this;
}
});
PX.ProductListView = Backbone.View.extend({
tagName: "ul",
className: "products",
// initialize: function (options) {
// this.container = options.container;
// },
render: function () {
var i, len = this.collection.length;
for (i=0; i < len; i++) {
this.renderItem(this.collection.models[i]);
};
$(this.container).find(this.className).remove();
this.$el.appendTo(this.options.container);
return this;
},
renderItem: function (model) {
var item = new PX.ProductView({
"model": model
});
item.render().$el.appendTo(this.$el);
}
});
// application
PX.App = Backbone.Router.extend({
routes: {
"/": "listProducts",
"list": "listProducts"
},
//initialize: function (options) {},
listProducts: function () {
var productsList = new PX.ProductListView({
"container": $('#container'),
"collection": PX.products
});
PX.products.deferred.done(function () {
productsList.render();
});
}
});
// bootstrap
PX.app = new PX.App();
Backbone.history.start();
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>API index</title>
</head>
<body>
<section>
<h1>Nouns...</h1>
<p>
/products<br>
/products/:id
</p>
</section>
<script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
</body>
</html>
// jQuery snippets used in the console to use the REST api created with app.js
// CREATE
jQuery.post("/api/products", {
"title": "My Awesome T-shirt",
"description": "All about the details. Of course it's black.",
"images": [
{
"kind": "thumbnail",
"url": "images/products/1234/main.jpg"
}
],
"categories": [
{ "name": "Clothes" },
{ "name": "Shirts" }
],
"style": "1234",
"variants": [
{
"color": "Black",
"images": [
{
"kind": "thumbnail",
"url": "images/products/1234/thumbnail.jpg"
},
{
"kind": "catalog",
"url": "images/products/1234/black.jpg"
}
],
"sizes": [
{
"size": "S",
"available": 10,
"sku": "CAT-1234-Blk-S",
"price": 99.99
},
{
"size": "M",
"available": 7,
"sku": "CAT-1234-Blk-M",
"price": 109.99
}
]
}
],
"catalogs": [
{ "name": "Apparel" }
]
}, function(data, textStatus, jqXHR) {
console.log("Post resposne:"); console.dir(data); console.log(textStatus); console.dir(jqXHR);
});
// generated a product document with automatically assigned ID, e.g. 4f34734d21289c1c28000007
// READ
jQuery.get("/api/products/", function(data, textStatus, jqXHR) {
console.log("Post resposne:");
console.dir(data);
console.log(textStatus);
console.dir(jqXHR);
});
jQuery.get("/api/products/4f34734d21289c1c28000007", function(data, textStatus, jqXHR) {
console.log("Post resposne:");
console.dir(data);
console.log(textStatus);
console.dir(jqXHR);
});
// UPDATE
jQuery.ajax({
url: "/api/products/4f34734d21289c1c28000007",
type: "PUT",
data: {
"title": "My Awesome T-shirt",
"description": "All about the details. Of course it's black, and longsleeve.",
"images": [
{
"kind": "thumbnail",
"url": "images/products/1234/main.jpg"
}
],
"categories": [
{ "name": "Clothes" },
{ "name": "Shirts" }
],
"style": "1234",
"variants": [
{
"color": "Black",
"images": [
{
"kind": "zoom",
"url": "images/products/1234/zoom.jpg"
}
],
"sizes": [
{
"size": "L",
"available": 77,
"sku": "CAT-1234-Blk-L",
"price": 99.99
}
]
}
],
"catalogs": [
{ "name": "Apparel" }
]
},
success: function(data, textStatus, jqXHR) {
console.log("PUT resposne:");
console.dir(data);
console.log(textStatus);
console.dir(jqXHR);
}
});
// Delete
jQuery.ajax({url: "/api/products/4f34734d21289c1c28000007", type: "DELETE", success: function(data, textStatus, jqXHR) { console.dir(data); }});
@jarndt
Copy link

jarndt commented Oct 16, 2013

Nicely done. Only thing I might suggest is changing the structure of your variants to an array of objects similar to:

[{" colour":"black", "size":"L", "skuid":"222222", "price":"99.99",...},..]

Again, great job

@xieweizhi
Copy link

great job , thanks

Copy link

ghost commented Jan 11, 2014

I am really new with server-side JS, it will help me to link to a JSON format feed to my Schema collections and
display a nice website.

Thanks nice job indeed !

@jumoog
Copy link

jumoog commented Feb 24, 2014

maybe you should add https://www.npmjs.org/package/connect-nocache to prevent caching

@chunta
Copy link

chunta commented Mar 29, 2015

I have one question. Web page exist in my web root dir. Web service use 80 port. Nodejs RESTful servcer use 8080. I got ' No Access-Control-Allo-Origin ....'. If data is defined as 'jsonp', i got unexpected token.

@nasihere
Copy link

very nice thanks

@gpraveencr
Copy link

Nicely done, thank you

@jeneg
Copy link

jeneg commented Dec 21, 2015

Nice nontrivial example, thanks!

@kaisLabiedh
Copy link

good job

@karuppasamy6492
Copy link

good job

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment