├── README.markdown ├── hello_world.js ├── lib └── node-router.js └── package.json /README.markdown: -------------------------------------------------------------------------------- 1 | # node-router 2 | 3 | Node-router is a small simple node.js http server that makes building simple web-services super simple. 4 | 5 | ## Node libraries 6 | 7 | There are two ways to include this library in your node project. You can either copy the node-router.js file in 8 | the same directory as your script and require it with a relative path: 9 | 10 | var NodeRouter = require('./node-router'); 11 | 12 | Or you can copy `node-router.js` to somewhere in your `require.paths` array. Then you can use a global require 13 | like: 14 | 15 | var NodeRouter = require('node-router'); 16 | 17 | See the [node docs][] for more details. 18 | 19 | [node docs]: http://nodejs.org/api.html#_modules 20 | 21 | -------------------------------------------------------------------------------- /hello_world.js: -------------------------------------------------------------------------------- 1 | var server = require('./lib/node-router').getServer(); 2 | 3 | server.get("/json", function (req, res, match) { 4 | return {hello: "World"}; 5 | }); 6 | 7 | server.get(new RegExp("^/(.*)$"), function hello(req, res, match) { 8 | return "Hello " + (match || "World") + "!"; 9 | }); 10 | 11 | 12 | server.listen(8080); -------------------------------------------------------------------------------- /lib/node-router.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2010 Tim Caswell 3 | 4 | Permission is hereby granted, free of charge, to any person 5 | obtaining a copy of this software and associated documentation 6 | files (the "Software"), to deal in the Software without 7 | restriction, including without limitation the rights to use, 8 | copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | 26 | var sys = require('sys'); 27 | var fs = require('fs'); 28 | var path = require('path'); 29 | var http = require('http'); 30 | var url_parse = require("url").parse; 31 | 32 | // Used as a simple, convient 404 handler. 33 | function notFound(req, res, message) { 34 | message = (message || "Not Found\n") + ""; 35 | res.writeHead(404, { 36 | "Content-Type": "text/plain", 37 | "Content-Length": message.length 38 | }); 39 | if (req.method !== "HEAD") 40 | res.write(message); 41 | res.end(); 42 | } 43 | 44 | // Modifies req and res to call logger with a log line on each res.end 45 | // Think of it as "middleware" 46 | function logify(req, res, logger) { 47 | var end = res.end; 48 | res.end = function () { 49 | // Common Log Format (mostly) 50 | logger((req.socket && req.socket.remoteAddress) + " - - [" + (new Date()).toUTCString() + "]" 51 | + " \"" + req.method + " " + req.url 52 | + " HTTP/" + req.httpVersionMajor + "." + req.httpVersionMinor + "\" " 53 | + res.statusCode + " - \"" 54 | + (req.headers['referer'] || "") + "\" \"" + (req.headers["user-agent"] ? req.headers["user-agent"].split(' ')[0] : '') + "\""); 55 | return end.apply(this, arguments); 56 | } 57 | var writeHead = res.writeHead; 58 | res.writeHead = function (code) { 59 | res.statusCode = code; 60 | return writeHead.apply(this, arguments); 61 | } 62 | } 63 | 64 | exports.getServer = function getServer(logger) { 65 | 66 | logger = logger || sys.puts; 67 | 68 | var routes = []; 69 | 70 | // Adds a route the the current server 71 | function addRoute(method, pattern, handler, format) { 72 | if (typeof pattern === 'string') { 73 | pattern = new RegExp("^" + pattern + "$"); 74 | } 75 | var route = { 76 | method: method, 77 | pattern: pattern, 78 | handler: handler 79 | }; 80 | if (format !== undefined) { 81 | route.format = format; 82 | } 83 | routes.push(route); 84 | } 85 | 86 | // The four verbs are wrappers around addRoute 87 | function get(pattern, handler) { 88 | return addRoute("GET", pattern, handler); 89 | } 90 | function post(pattern, handler, format) { 91 | return addRoute("POST", pattern, handler, format); 92 | } 93 | function put(pattern, handler, format) { 94 | return addRoute("PUT", pattern, handler, format); 95 | } 96 | function del(pattern, handler) { 97 | return addRoute("DELETE", pattern, handler); 98 | } 99 | function head(pattern, handler) { 100 | return addRoute("HEAD", pattern, handler); 101 | } 102 | 103 | // This is a meta pattern that expands to a common RESTful mapping 104 | function resource(name, controller, format) { 105 | get(new RegExp('^/' + name + '$'), controller.index); 106 | get(new RegExp('^/' + name + '/([^/]+)$'), controller.show); 107 | post(new RegExp('^/' + name + '$'), controller.create, format); 108 | put(new RegExp('^/' + name + '/([^/]+)$'), controller.update, format); 109 | del(new RegExp('^/' + name + '/([^/]+)$'), controller.destroy); 110 | }; 111 | 112 | function resourceController(name, data, on_change) { 113 | data = data || []; 114 | on_change = on_change || function () {}; 115 | return { 116 | index: function (req, res) { 117 | res.simpleJson(200, {content: data, self: '/' + name}); 118 | }, 119 | show: function (req, res, id) { 120 | var item = data[id]; 121 | if (item) { 122 | res.simpleJson(200, {content: item, self: '/' + name + '/' + id}); 123 | } else { 124 | res.notFound(); 125 | } 126 | }, 127 | create: function (req, res) { 128 | req.jsonBody(function (json) { 129 | var item, id, url; 130 | item = json && json.content; 131 | if (!item) { 132 | res.notFound(); 133 | } else { 134 | data.push(item); 135 | id = data.length - 1; 136 | on_change(id); 137 | url = "/" + name + "/" + id; 138 | res.simpleJson(201, {content: item, self: url}, [["Location", url]]); 139 | } 140 | }); 141 | }, 142 | update: function (req, res, id) { 143 | req.jsonBody(function (json) { 144 | var item = json && json.content; 145 | if (!item) { 146 | res.notFound(); 147 | } else { 148 | data[id] = item; 149 | on_change(id); 150 | res.simpleJson(200, {content: item, self: "/" + name + "/" + id}); 151 | } 152 | }); 153 | }, 154 | destroy: function (req, res, id) { 155 | delete data[id]; 156 | on_change(id); 157 | res.simpleJson(200, "200 Destroyed"); 158 | } 159 | }; 160 | }; 161 | 162 | // Create the http server object 163 | var server = http.createServer(function (req, res) { 164 | 165 | // Enable logging on all requests using common-logger style 166 | logify(req, res, logger); 167 | 168 | var uri, path; 169 | 170 | // Performs an HTTP 302 redirect 171 | res.redirect = function redirect(location) { 172 | res.writeHead(302, {"Location": location}); 173 | res.end(); 174 | } 175 | 176 | // Performs an internal redirect 177 | res.innerRedirect = function innerRedirect(location) { 178 | logger("Internal Redirect: " + req.url + " -> " + location); 179 | req.url = location; 180 | doRoute(); 181 | } 182 | 183 | function simpleResponse(code, body, content_type, extra_headers) { 184 | res.writeHead(code, (extra_headers || []).concat( 185 | [ ["Content-Type", content_type], 186 | ["Content-Length", Buffer.byteLength(body, 'utf8')] 187 | ])); 188 | if (req.method !== "HEAD") 189 | res.write(body, 'utf8'); 190 | res.end(); 191 | } 192 | 193 | res.simpleText = function (code, body, extra_headers) { 194 | simpleResponse(code, body, "text/plain", extra_headers); 195 | }; 196 | 197 | res.simpleHtml = function (code, body, extra_headers) { 198 | simpleResponse(code, body, "text/html", extra_headers); 199 | }; 200 | 201 | res.simpleJson = function (code, json, extra_headers) { 202 | simpleResponse(code, JSON.stringify(json), "application/json", extra_headers); 203 | }; 204 | 205 | res.notFound = function (message) { 206 | notFound(req, res, message); 207 | }; 208 | 209 | res.onlyHead = function (code, extra_headers) { 210 | res.writeHead(code, (extra_headers || []).concat( 211 | [["Content-Type", content_type]])); 212 | res.end(); 213 | } 214 | 215 | function doRoute() { 216 | uri = url_parse(req.url); 217 | path = uri.pathname; 218 | 219 | for (var i = 0, l = routes.length; i < l; i += 1) { 220 | var route = routes[i]; 221 | if (req.method === route.method) { 222 | var match = path.match(route.pattern); 223 | if (match && match[0].length > 0) { 224 | match.shift(); 225 | match = match.map(function (part) { 226 | return part ? unescape(part) : part; 227 | }); 228 | match.unshift(res); 229 | match.unshift(req); 230 | if (route.format !== undefined) { 231 | var body = ""; 232 | req.setEncoding('utf8'); 233 | req.addListener('data', function (chunk) { 234 | body += chunk; 235 | }); 236 | req.addListener('end', function () { 237 | if (route.format === 'json') { 238 | try { 239 | body = JSON.parse(unescape(body)); 240 | } catch(e) { 241 | body = null; 242 | } 243 | } 244 | match.push(body); 245 | route.handler.apply(null, match); 246 | }); 247 | return; 248 | } 249 | var result = route.handler.apply(null, match); 250 | switch (typeof result) { 251 | case "string": 252 | res.simpleHtml(200, result); 253 | break; 254 | case "object": 255 | res.simpleJson(200, result); 256 | break; 257 | } 258 | 259 | return; 260 | } 261 | } 262 | } 263 | 264 | notFound(req, res); 265 | } 266 | doRoute(); 267 | 268 | }); 269 | 270 | 271 | function listen(port, host, callback) { 272 | port = port || 8080; 273 | 274 | if (typeof host === 'undefined' || host == '*') 275 | host = null; 276 | 277 | server.listen(port, host, callback); 278 | 279 | if (typeof port === 'number') { 280 | logger("node-router server instance at http://" + (host || '*') + ":" + port + "/"); 281 | } else { 282 | logger("node-router server instance at unix:" + port); 283 | } 284 | } 285 | 286 | function end() { 287 | return server.end(); 288 | } 289 | 290 | // Return a handle to the public facing functions from this closure as the 291 | // server object. 292 | return { 293 | get: get, 294 | post: post, 295 | put: put, 296 | del: del, 297 | resource: resource, 298 | resourceController: resourceController, 299 | listen: listen, 300 | end: end 301 | }; 302 | } 303 | 304 | 305 | 306 | 307 | exports.staticHandler = function (filename) { 308 | var body, headers; 309 | var content_type = mime.getMime(filename) 310 | var encoding = (content_type.slice(0,4) === "text" ? "utf8" : "binary"); 311 | 312 | function loadResponseData(req, res, callback) { 313 | if (body && headers) { 314 | callback(); 315 | return; 316 | } 317 | 318 | fs.readFile(filename, encoding, function (err, data) { 319 | if (err) { 320 | notFound(req, res, "Cannot find file: " + filename); 321 | return; 322 | } 323 | body = data; 324 | headers = [ [ "Content-Type" , content_type ], 325 | [ "Content-Length" , body.length ] 326 | ]; 327 | headers.push(["Cache-Control", "public"]); 328 | 329 | callback(); 330 | }); 331 | } 332 | 333 | return function (req, res) { 334 | loadResponseData(req, res, function () { 335 | res.writeHead(200, headers); 336 | if (req.method !== "HEAD") 337 | res.write(body, encoding); 338 | res.end(); 339 | }); 340 | }; 341 | }; 342 | 343 | exports.staticDirHandler = function(root, prefix) { 344 | function loadResponseData(req, res, filename, callback) { 345 | var content_type = mime.getMime(filename); 346 | var encoding = (content_type.slice(0,4) === "text" ? "utf8" : "binary"); 347 | 348 | fs.readFile(filename, encoding, function(err, data) { 349 | if(err) { 350 | notFound(req, res, "Cannot find file: " + filename); 351 | return; 352 | } 353 | var headers = [ [ "Content-Type" , content_type ], 354 | [ "Content-Length" , data.length ], 355 | [ "Cache-Control" , "public" ] 356 | ]; 357 | callback(headers, data, encoding); 358 | }); 359 | } 360 | 361 | return function (req, res) { 362 | // trim off any query/anchor stuff 363 | var filename = req.url.replace(/[\?|#].*$/, ''); 364 | if (prefix) filename = filename.replace(new RegExp('^'+prefix), ''); 365 | // make sure nobody can explore our local filesystem 366 | filename = path.join(root, filename.replace(/\.\.+/g, '.')); 367 | if (filename == root) filename = path.join(root, 'index.html'); 368 | loadResponseData(req, res, filename, function(headers, body, encoding) { 369 | res.writeHead(200, headers); 370 | if (req.method !== "HEAD") 371 | res.write(body, encoding); 372 | res.end(); 373 | }); 374 | }; 375 | }; 376 | 377 | 378 | // Mini mime module for static file serving 379 | var DEFAULT_MIME = 'application/octet-stream'; 380 | var mime = exports.mime = { 381 | 382 | getMime: function getMime(path) { 383 | var index = path.lastIndexOf("."); 384 | if (index < 0) { 385 | return DEFAULT_MIME; 386 | } 387 | return mime.TYPES[path.substring(index).toLowerCase()] || DEFAULT_MIME; 388 | }, 389 | 390 | TYPES : { ".3gp" : "video/3gpp", 391 | ".a" : "application/octet-stream", 392 | ".ai" : "application/postscript", 393 | ".aif" : "audio/x-aiff", 394 | ".aiff" : "audio/x-aiff", 395 | ".asc" : "application/pgp-signature", 396 | ".asf" : "video/x-ms-asf", 397 | ".asm" : "text/x-asm", 398 | ".asx" : "video/x-ms-asf", 399 | ".atom" : "application/atom+xml", 400 | ".au" : "audio/basic", 401 | ".avi" : "video/x-msvideo", 402 | ".bat" : "application/x-msdownload", 403 | ".bin" : "application/octet-stream", 404 | ".bmp" : "image/bmp", 405 | ".bz2" : "application/x-bzip2", 406 | ".c" : "text/x-c", 407 | ".cab" : "application/vnd.ms-cab-compressed", 408 | ".cc" : "text/x-c", 409 | ".chm" : "application/vnd.ms-htmlhelp", 410 | ".class" : "application/octet-stream", 411 | ".com" : "application/x-msdownload", 412 | ".conf" : "text/plain", 413 | ".cpp" : "text/x-c", 414 | ".crt" : "application/x-x509-ca-cert", 415 | ".css" : "text/css", 416 | ".csv" : "text/csv", 417 | ".cxx" : "text/x-c", 418 | ".deb" : "application/x-debian-package", 419 | ".der" : "application/x-x509-ca-cert", 420 | ".diff" : "text/x-diff", 421 | ".djv" : "image/vnd.djvu", 422 | ".djvu" : "image/vnd.djvu", 423 | ".dll" : "application/x-msdownload", 424 | ".dmg" : "application/octet-stream", 425 | ".doc" : "application/msword", 426 | ".dot" : "application/msword", 427 | ".dtd" : "application/xml-dtd", 428 | ".dvi" : "application/x-dvi", 429 | ".ear" : "application/java-archive", 430 | ".eml" : "message/rfc822", 431 | ".eps" : "application/postscript", 432 | ".exe" : "application/x-msdownload", 433 | ".f" : "text/x-fortran", 434 | ".f77" : "text/x-fortran", 435 | ".f90" : "text/x-fortran", 436 | ".flv" : "video/x-flv", 437 | ".for" : "text/x-fortran", 438 | ".gem" : "application/octet-stream", 439 | ".gemspec" : "text/x-script.ruby", 440 | ".gif" : "image/gif", 441 | ".gz" : "application/x-gzip", 442 | ".h" : "text/x-c", 443 | ".hh" : "text/x-c", 444 | ".htm" : "text/html", 445 | ".html" : "text/html", 446 | ".ico" : "image/vnd.microsoft.icon", 447 | ".ics" : "text/calendar", 448 | ".ifb" : "text/calendar", 449 | ".iso" : "application/octet-stream", 450 | ".jar" : "application/java-archive", 451 | ".java" : "text/x-java-source", 452 | ".jnlp" : "application/x-java-jnlp-file", 453 | ".jpeg" : "image/jpeg", 454 | ".jpg" : "image/jpeg", 455 | ".js" : "application/javascript", 456 | ".json" : "application/json", 457 | ".log" : "text/plain", 458 | ".m3u" : "audio/x-mpegurl", 459 | ".m4v" : "video/mp4", 460 | ".man" : "text/troff", 461 | ".mathml" : "application/mathml+xml", 462 | ".mbox" : "application/mbox", 463 | ".mdoc" : "text/troff", 464 | ".me" : "text/troff", 465 | ".mid" : "audio/midi", 466 | ".midi" : "audio/midi", 467 | ".mime" : "message/rfc822", 468 | ".mml" : "application/mathml+xml", 469 | ".mng" : "video/x-mng", 470 | ".mov" : "video/quicktime", 471 | ".mp3" : "audio/mpeg", 472 | ".mp4" : "video/mp4", 473 | ".mp4v" : "video/mp4", 474 | ".mpeg" : "video/mpeg", 475 | ".mpg" : "video/mpeg", 476 | ".ms" : "text/troff", 477 | ".msi" : "application/x-msdownload", 478 | ".odp" : "application/vnd.oasis.opendocument.presentation", 479 | ".ods" : "application/vnd.oasis.opendocument.spreadsheet", 480 | ".odt" : "application/vnd.oasis.opendocument.text", 481 | ".ogg" : "application/ogg", 482 | ".p" : "text/x-pascal", 483 | ".pas" : "text/x-pascal", 484 | ".pbm" : "image/x-portable-bitmap", 485 | ".pdf" : "application/pdf", 486 | ".pem" : "application/x-x509-ca-cert", 487 | ".pgm" : "image/x-portable-graymap", 488 | ".pgp" : "application/pgp-encrypted", 489 | ".pkg" : "application/octet-stream", 490 | ".pl" : "text/x-script.perl", 491 | ".pm" : "text/x-script.perl-module", 492 | ".png" : "image/png", 493 | ".pnm" : "image/x-portable-anymap", 494 | ".ppm" : "image/x-portable-pixmap", 495 | ".pps" : "application/vnd.ms-powerpoint", 496 | ".ppt" : "application/vnd.ms-powerpoint", 497 | ".ps" : "application/postscript", 498 | ".psd" : "image/vnd.adobe.photoshop", 499 | ".py" : "text/x-script.python", 500 | ".qt" : "video/quicktime", 501 | ".ra" : "audio/x-pn-realaudio", 502 | ".rake" : "text/x-script.ruby", 503 | ".ram" : "audio/x-pn-realaudio", 504 | ".rar" : "application/x-rar-compressed", 505 | ".rb" : "text/x-script.ruby", 506 | ".rdf" : "application/rdf+xml", 507 | ".roff" : "text/troff", 508 | ".rpm" : "application/x-redhat-package-manager", 509 | ".rss" : "application/rss+xml", 510 | ".rtf" : "application/rtf", 511 | ".ru" : "text/x-script.ruby", 512 | ".s" : "text/x-asm", 513 | ".sgm" : "text/sgml", 514 | ".sgml" : "text/sgml", 515 | ".sh" : "application/x-sh", 516 | ".sig" : "application/pgp-signature", 517 | ".snd" : "audio/basic", 518 | ".so" : "application/octet-stream", 519 | ".svg" : "image/svg+xml", 520 | ".svgz" : "image/svg+xml", 521 | ".swf" : "application/x-shockwave-flash", 522 | ".t" : "text/troff", 523 | ".tar" : "application/x-tar", 524 | ".tbz" : "application/x-bzip-compressed-tar", 525 | ".tci" : "application/x-topcloud", 526 | ".tcl" : "application/x-tcl", 527 | ".tex" : "application/x-tex", 528 | ".texi" : "application/x-texinfo", 529 | ".texinfo" : "application/x-texinfo", 530 | ".text" : "text/plain", 531 | ".tif" : "image/tiff", 532 | ".tiff" : "image/tiff", 533 | ".torrent" : "application/x-bittorrent", 534 | ".tr" : "text/troff", 535 | ".ttf" : "application/x-font-ttf", 536 | ".txt" : "text/plain", 537 | ".vcf" : "text/x-vcard", 538 | ".vcs" : "text/x-vcalendar", 539 | ".vrml" : "model/vrml", 540 | ".war" : "application/java-archive", 541 | ".wav" : "audio/x-wav", 542 | ".wma" : "audio/x-ms-wma", 543 | ".wmv" : "video/x-ms-wmv", 544 | ".wmx" : "video/x-ms-wmx", 545 | ".wrl" : "model/vrml", 546 | ".wsdl" : "application/wsdl+xml", 547 | ".xbm" : "image/x-xbitmap", 548 | ".xhtml" : "application/xhtml+xml", 549 | ".xls" : "application/vnd.ms-excel", 550 | ".xml" : "application/xml", 551 | ".xpm" : "image/x-xpixmap", 552 | ".xsl" : "application/xml", 553 | ".xslt" : "application/xslt+xml", 554 | ".yaml" : "text/yaml", 555 | ".yml" : "text/yaml", 556 | ".zip" : "application/zip" 557 | } 558 | }; 559 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { "name" : "node-router" 2 | , "version" : "0.0.3" 3 | , "description" : "A simple http server for node.js that has sinatra like qualities. Ideal for generating web services via node." 4 | , "author" : "Tim Caswell " 5 | , "main" : "lib/node-router" 6 | , "engines" : { "node" : ">=0.1.90" } 7 | } 8 | --------------------------------------------------------------------------------