node.js essentials - luzala.orgluzala.org/node.js essentials.pdf · node.js essentials credits...
Post on 21-Sep-2020
3 Views
Preview:
TRANSCRIPT
Node.jsEssentials
TableofContents
Node.jsEssentials
Credits
AbouttheAuthor
AbouttheReviewer
www.PacktPub.com
Supportfiles,eBooks,discountoffers,andmore
Whysubscribe?
FreeaccessforPacktaccountholders
Preface
Whatthisbookcovers
Whatyouneedforthisbook
Whothisbookisfor
Conventions
Readerfeedback
Customersupport
Downloadingtheexamplecode
Errata
Piracy
Questions
1.GettingStarted
Settingup
Hellorequire
Hellonpm
Summary
2.SimpleHTTP
Introducingrouting
Summary
3.Authentication
Basicauthentication
Bearertokens
OAuth
Summary
4.Debugging
Logging
Errorhandling
Summary
5.Configuration
JSONfiles
Environmentalvariables
Arguments
Summary
6.LevelDBandNoSQL
LevelDB
MongoDB
Summary
7.Socket.IO
Rooms
Authentication
Summary
8.CreatingandDeployingPackages
Creatingnpmpackages
Summary
9.UnitTesting
Installingmocha
Chai
Stubbingmethods
Summary
10.UsingMoreThanJavaScript
CoffeeScript
Codeblocksandfunctions
Theexistentialoperator
Objectsandarrays
Classes
Summary
Index
Node.jsEssentials
Node.jsEssentialsCopyright©2015PacktPublishing
Allrightsreserved.Nopartofthisbookmaybereproduced,storedinaretrievalsystem,ortransmittedinanyformorbyanymeans,withoutthepriorwrittenpermissionofthepublisher,exceptinthecaseofbriefquotationsembeddedincriticalarticlesorreviews.
Everyefforthasbeenmadeinthepreparationofthisbooktoensuretheaccuracyoftheinformationpresented.However,theinformationcontainedinthisbookissoldwithoutwarranty,eitherexpressorimplied.Neithertheauthor,norPacktPublishing,anditsdealersanddistributorswillbeheldliableforanydamagescausedorallegedtobecauseddirectlyorindirectlybythisbook.
PacktPublishinghasendeavoredtoprovidetrademarkinformationaboutallofthecompaniesandproductsmentionedinthisbookbytheappropriateuseofcapitals.However,PacktPublishingcannotguaranteetheaccuracyofthisinformation.
Firstpublished:November2015
Productionreference:1301015
PublishedbyPacktPublishingLtd.
LiveryPlace
35LiveryStreet
BirminghamB32PB,UK.
ISBN978-1-78528-492-2
www.packtpub.com
CreditsAuthor
FabianCook
Reviewers
ShoubhikBose
GlennGeenen
CommissioningEditor
EdwardGordan
AcquisitionEditor
DivyaPoojari
ContentDevelopmentEditor
AthiraLaji
TechnicalEditor
NaveenkumarJain
CopyEditor
SnehaSingh
ProjectCoordinator
HarshalVed
Proofreader
SafisEditing
Indexer
HemanginiBari
ProductionCoordinator
ShantanuN.Zagade
CoverWork
ShantanuN.Zagade
AbouttheAuthorFabianCookisanexperiencedJavaScriptdeveloperwholivesinHawkesBay,NewZealand.HebeganworkingwithJavaandC#veryearlyinhislife,whichleadtousingNode.jsinanopensourcecontext.HeisnowcurrentlyworkingforaNewZealandISP,knownasNOWNZwheretheyareutilizingthefullpowerofNode.js,DockerandCoreOS.
AbouttheReviewerGlennGeenenisaNode.jsdeveloperwithabackgroundingameandmobiledevelopment.HehasmostlyworkedasaniOSconsultantbeforebecomingaNode.jsconsultantforhiscompany,GeenenTijd.
www.PacktPub.com
Supportfiles,eBooks,discountoffers,andmoreForsupportfilesanddownloadsrelatedtoyourbook,pleasevisitwww.PacktPub.com.
DidyouknowthatPacktofferseBookversionsofeverybookpublished,withPDFandePubfilesavailable?YoucanupgradetotheeBookversionatwww.PacktPub.comandasaprintbookcustomer,youareentitledtoadiscountontheeBookcopy.Getintouchwithusat<service@packtpub.com>formoredetails.
Atwww.PacktPub.com,youcanalsoreadacollectionoffreetechnicalarticles,signupforarangeoffreenewslettersandreceiveexclusivediscountsandoffersonPacktbooksandeBooks.
https://www2.packtpub.com/books/subscription/packtlib
DoyouneedinstantsolutionstoyourITquestions?PacktLibisPackt’sonlinedigitalbooklibrary.Here,youcansearch,access,andreadPackt’sentirelibraryofbooks.
Whysubscribe?FullysearchableacrosseverybookpublishedbyPacktCopyandpaste,print,andbookmarkcontentOndemandandaccessibleviaawebbrowser
FreeaccessforPacktaccountholdersIfyouhaveanaccountwithPacktatwww.PacktPub.com,youcanusethistoaccessPacktLibtodayandview9entirelyfreebooks.Simplyuseyourlogincredentialsforimmediateaccess.
PrefaceNode.jsissimplyatoolthatletsyouuseJavaScriptontheserverside.However,itactuallydoesmuchmorethanthat–byextendingJavaScript,itallowsforamuchmoreintegratedandefficientapproachtodevelopment.Itcomesasnosurprisethatit’safundamentaltoolforfull-stackJavaScriptdevelopers.Whetheryouworkonthebackendorfrontend,youadoptamuchmorecollaborativeandagilewayofworkingusingNode.js,sothatyouandyourteamcanfocusondeliveringaqualityendproduct.Thiswillensurethatyou’rereadytotakeonanynewchallengethatgetsthrownatyou.
Thisbookwillbefastpacedandcoverdependencymanagement,runningyourownHTTPserver,realtimecommunication,andeverythinginbetweenthatisneededtogetupandrunningwithNode.js.
WhatthisbookcoversChapter1,GettingStarted,coversthesetupofNode.js.Youwillalsocoverhowtoutilizeandmanagedependencies.
Chapter2,SimpleHTTP,covershowtorunasimpleHTTPserverandhelpsyouunderstandroutingandutilizationofmiddleware.
Chapter3,Authentication,coverstheutilizationofmiddlewareandJSONWebTokentoauthenticateusers.
Chapter4,Debugging,coverstheintegrationofpost-mortemtechniquesinyourdevelopmenttasksandhowtodebugyourNode.jsprograms.
Chapter5,Configuration,coverstheconfigurationandmaintenanceofyoursoftwareusingcentralizedconfigurationoptions,arguments,andenvironmentalvariables.
Chapter6,LevelDBandNoSQL,coverstheintroductionofNoSQLdatabases,suchasLevelDBandMongoDB.Italsocoverstheuseofthesimplekey/valuestoreandamorecompletedocumentdatabase.
Chapter7,Socket.IO,exploresthereal-timecommunicationbetweenclients,servers,andbackagainandalsohowitauthenticatesandnotifiestheusers.
Chapter8,CreatingandDeployingPackages,focusesonsharingthemodulesandcontributingtotheeco-system
Chapter9,UnitTesting,testsyourcodeusingMocha,Sinon,andChanceandalsocovershowtousemockswithfunctionsandgeneraterandomvaluestotestyourcode
Chapter10,UsingMoreThanJavaScript,explainstheusageofCoffeeScriptwithNode.jstoexpandlanguagecapabilities.
WhatyouneedforthisbookYouwillneedacomputerthatrunsUnix(Macintosh),LinuxorWindows,alongwithyourpreferredIntegratedDevelopmentEnvironment.Ifyoudon’thaveanIDEthenyouhaveafewoptions,suchas:
Atom:https://atom.io/Sublime:http://www.sublimetext.com/Cloud9:https://c9.io/
WhothisbookisforThebookwillbehelpfultoanybodywhowantstohaveknowledgeofNode.js(whatNode.jsisabout,howtouseit,whereit’susefulandwhentouseit).Familiaritywithserver-sideandNode.jsisaprerequisite.
ConventionsInthisbook,youwillfindanumberofstylesoftextthatdistinguishbetweendifferentkindsofinformation.Herearesomeexamplesofthesestyles,andanexplanationoftheirmeaning.
Codewordsintext,databasetablenames,foldernames,filenames,fileextensions,pathnames,dummyURLs,userinput,andTwitterhandlesareshownasfollows:“Wecanincludeothercontextsthroughtheuseoftheincludedirective.”
Ablockofcodeissetasfollows:
<scripttype='application/javascript'src='script_a.js'></script>
<scripttype='application/javascript'src='script_b.js'></script>
Anycommand-lineinputoroutputiswrittenasfollows:
[~]$npminstall-gn
Newtermsandimportantwordsareshowninbold.Wordsthatyouseeonthescreen,inmenusordialogboxesforexample,appearinthetextlikethis:“Iftheuserhasn’tpassedboththeusernameandpasswordtheserverwillreturn500BadRequest“.
NoteWarningsorimportantnotesappearinaboxlikethis.
TipTipsandtricksappearlikethis.
ReaderfeedbackFeedbackfromourreadersisalwayswelcome.Letusknowwhatyouthinkaboutthisbook—whatyoulikedormayhavedisliked.Readerfeedbackisimportantforustodeveloptitlesthatyoureallygetthemostoutof.
Tosendusgeneralfeedback,simplysendane-mailto<feedback@packtpub.com>,andmentionthebooktitleviathesubjectofyourmessage.
Ifthereisatopicthatyouhaveexpertiseinandyouareinterestedineitherwritingorcontributingtoabook,seeourauthorguideonwww.packtpub.com/authors.
CustomersupportNowthatyouaretheproudownerofaPacktbook,wehaveanumberofthingstohelpyoutogetthemostfromyourpurchase.
DownloadingtheexamplecodeYoucandownloadtheexamplecodefilesforallPacktbooksyouhavepurchasedfromyouraccountathttp://www.packtpub.com.Ifyoupurchasedthisbookelsewhere,youcanvisithttp://www.packtpub.com/supportandregistertohavethefilese-maileddirectlytoyou.
ErrataAlthoughwehavetakeneverycaretoensuretheaccuracyofourcontent,mistakesdohappen.Ifyoufindamistakeinoneofourbooks—maybeamistakeinthetextorthecode—wewouldbegratefulifyouwouldreportthistous.Bydoingso,youcansaveotherreadersfromfrustrationandhelpusimprovesubsequentversionsofthisbook.Ifyoufindanyerrata,pleasereportthembyvisitinghttp://www.packtpub.com/submit-errata,selectingyourbook,clickingontheerratasubmissionformlink,andenteringthedetailsofyourerrata.Onceyourerrataareverified,yoursubmissionwillbeacceptedandtheerratawillbeuploadedonourwebsite,oraddedtoanylistofexistingerrata,undertheErratasectionofthattitle.Anyexistingerratacanbeviewedbyselectingyourtitlefromhttp://www.packtpub.com/support.
PiracyPiracyofcopyrightmaterialontheInternetisanongoingproblemacrossallmedia.AtPackt,wetaketheprotectionofourcopyrightandlicensesveryseriously.Ifyoucomeacrossanyillegalcopiesofourworks,inanyform,ontheInternet,pleaseprovideuswiththelocationaddressorwebsitenameimmediatelysothatwecanpursuearemedy.
Pleasecontactusat<copyright@packtpub.com>withalinktothesuspectedpiratedmaterial.
Weappreciateyourhelpinprotectingourauthors,andourabilitytobringyouvaluablecontent.
QuestionsYoucancontactusat<questions@packtpub.com>ifyouarehavingaproblemwithanyaspectofthebook,andwewilldoourbesttoaddressit.
Chapter1.GettingStartedEveryWebdevelopermusthavecomeacrossiteveryonceinawhile,eveniftheyjustdabbleinsimpleWebpages.WheneveryouwanttomakeyourWebpagealittlemoreinteractive,yougrabyourtrustworthyfriends,suchasJavaScriptandjQuery,andhacktogethersomethingnew.YoumighthavedevelopedsomeexcitingfrontendapplicationsusingAngularJSorBackboneandwanttolearnmoreaboutwhatelseyoucandowithJavaScript.
WhiletestingyourwebsiteonmultiplebrowsersyoumusthavecomeacrossGoogleChromeatsomepointandyoumighthavenoticedthatitisagreatplatformforJavaScriptapplications.
GoogleChromeandNode.jshavesomethingverybigincommon:theybothworkonGoogle’shigh-performanceV8JavaScriptengine,thisgivesusthesameengineinthebrowserthatwewillbeusinginthebackend,prettycool,right?
SettingupInordertogetstartedanduseNode.js,weneedtodownloadandinstallNode.js.Thebestwaytoinstallitwillbetoheadovertohttps://nodejs.org/anddownloadtheinstaller.
Atthetimeofwriting,thecurrentversionofNode.jsis4.2.1.
Toensureconsistency,wearegoingtouseanpmpackagetoinstallthecorrectversionofNode.JSand,forthis,wearegoingtousethenpackagedescribedathttps://www.npmjs.com/package/n.
Currently,thispackagehassupportonlyfor*nixmachines.ForWindows.seenvm-windowsordownloadthebinaryfor4.2.1fromhttps://nodejs.org/dist/v4.2.1/.
OnceyouhaveNode.jsinstalled,openaterminalandrun:
[~]$npminstall-gn
The–gargumentwillinstallthepackagegloballysowecanusethepackageanywhere.
Linuxusersmayneedtoruncommandsthatinstallglobalpackagesassudo.
Usingtherecentlyinstallpackage,run:
[~]$n
Thiswilldisplayascreenwiththefollowingpackages:
node/0.10.38
node/0.11.16
node/0.12.0
node/0.12.7
node/4.2.1
Ifnode/4.2.1isn’tmarkedwecansimplyrunthefollowingpackages;thiswillensurethatnode/4.2.1getsinstalled:
[~]$sudon4.2.1
Toensurethatthenodeisgood-to-go,letscreateandrunasimplehelloworldexample:
[~/src/examples/example-1]$touchexample.js
[~/src/examples/example-1]$echo"console.log(\"Helloworld\")">
example.js
[~/src/examples/example-1]$nodeexample.js
HelloWorld
Cool,itworks;nowlet’sgetdowntobusiness.
HellorequireIntheprecedingexample,wejustloggedasimplemessage,nothinginteresting,solet’sdiveabitdeeperinthissection.
Whenusingmultiplescriptsinthebrowser,weusuallyjustincludeanotherscripttagsuchas:
<scripttype='application/javascript'src='script_a.js'></script>
<scripttype='application/javascript'src='script_b.js'></script>
Boththesescriptssharethesameglobalscope,thisusuallyleadstosomeunusualconflictswhenpeoplewanttogivevariablesthesamename.
//script_a.js
functionrun(){
console.log("I'mrunningfromscript_a.js!");
}
$(run);
//script_b.js
functionrun(){
console.log("I'mrunningfromscript_b.js!");
}
$(run);
Thiscanleadtoconfusion,andwhenmanyfilesareminifiedandcrammedtogetheritcausesaproblem;script_adeclaresaglobalvariable,whichisthendeclaredagaininscript_band,onrunningthecode,weseethefollowingontheconsole:
>I'mrunningfromscript_b.js!
>I'mrunningfromscript_b.js!
Themostcommonmethodtogetaroundthisandtolimitthepollutionoftheglobalscopeistowrapourfileswithananonymousfunction,asshown:
//script_a.js
(function($,undefined){
functionrun(){
console.log("I'mrunningfromscript_a.js!");
}
$(run);
})(jQuery);
//script_b.js
(function($,undefined){
functionrun(){
console.log("I'mrunningfromscript_b.js!");
}
$(run);
})(jQuery);
Nowwhenwerunthis,itworksasexpected:
>I'mrunningfromscript_a.js!
>I'mrunningfromscript_b.js!
Thisisgoodforcodethatisn’tdependeduponexternally,butwhatdowedoforthecodethatis?Wejustexportit,right?
Somethingsimilartothefollowingcodewilldo:
(function(undefined){
functionLogger(){
}
Logger.prototype.log=function(message/*...*/){
console.log.apply(console,arguments);
}
this.Logger=Logger;
})()
Now,whenwerunthisscript,wecanaccessLoggerfromtheglobalscope:
varlogger=newLogger();
logger.log("This","is","pretty","cool")
>Thisisprettycool
Sonowwecanshareourlibrariesandeverythingisgood;ButwhatifsomeoneelsealreadyhasalibrarythatexposesthesameLoggerclass.
Whatdoesnodedotosolvethisissue?Hellorequire!
Node.jshasasimplewaytobringinscriptsandmodulesfromexternalsources,comparabletorequireinPHP.
Letscreateafewfilesinthisstructure:
/example-2
/util
index.js
logger.js
main.js
/*util/index.js*/
varlogger=newLogger()
varutil={
logger:logger
};
/*util/logger.js*/
functionLogger(){
}
Logger.prototype.log=function(message/*...*/){
console.log.apply(console,arguments);
};
/*main.js*/
util.logger.log("Thisisprettycool");
Wecanseethatmain.js.isdependentonutil/index.js,whichisinturndependentonutil/logger.js.
Thisshouldjustworkright?Maybenot.Let’srunthecommand:
[~/src/examples/example-2]$nodemain.js
ReferenceError:loggerisnotdefined
atObject.<anonymous>(/Users/fabian/examples/example-2/main.js:1:63)
/*Removedforsimplicity*/
atNode.js:814:3
Sowhyisthis?Shouldn’ttheybesharingthesameglobalscope?Well,inNode.jsthestoryisabitdifferent.Rememberthoseanonymousfunctionsthatwewerewrappingourfilesinearlier?Node.jswrapsourscriptsinthemautomaticallyandthisiswhererequirefitsin.
Letsfixourfiles,asshown:
/*util/index.js*/
Logger=require("./logger")
/*main.js*/
util=require("./util");
Ifyounotice,Ididn’tuseindex.jswhenrequiringutil/index.js;thereasonforthisisthatwhenyouarequireafolderratherthanafileyoucanspecifyanindexfilethatcanrepresentthatfolder’scode.Thiscanbehandyforsomethingsuchasamodelfolderwhereyouexposeallyourmodelsinonerequireratherthanhavingaseparaterequireforeachmodel.
Sonow,wehaverequiredourfiles.Butwhatdowegetback?
[~/src/examples/example-2]$node
>varutil=require("./util");
>console.log(util);
{}
Still,thereisnologger.Wehavemissedanimportantstep;wehaven’ttoldNode.jswhatwewanttoexposeinourfiles.
ToexposesomethinginNode.js,weuseanobjectcalledmodule.exports.Thereisashorthandreferencetoitthatisjustexports.Whenourfileiswrappedinananonymousfunction,bothmoduleandexportsarepassedasaparameter,asshowninthefollowingexample:
functionModule(){
this.exports={};
}
functionrequire(file){
//.....
returnsmodule.exports;
}
varmodule=newModule();
varexports=module.exports;
(function(exports,require,module){
exports="Valuea"
module.exports="Valueb"
})(exports,require,module);
console.log(module.exports);
//Valueb
TipDownloadingtheexamplecode
YoucandownloadtheexamplecodefilesforallPacktbooksyouhavepurchasedfromyouraccountathttp://www.packtpub.com.Ifyoupurchasedthisbookelsewhere,youcanvisithttp://www.packtpub.com/supportandregistertohavethefilese-maileddirectlytoyou.
Theexampleshowsthatexportsisinitiallyjustareferencetomodule.exports.Thismeansthat,ifyouuseexports={},thevalueyousetitaswon’tbeaccessibleoutsidethefunction’sscope.However,whenyouaddpropertiestoanexportsobject,youareactuallyaddingpropertiestothemodule.exportsobjectastheyareboththesamevalue.Assigningavaluetomodule.exportswillexportthatvaluebecauseitisaccessibleoutsidethefunction’sscopethroughthemodule.
Withthisknowledge,wecanfinallyrunourscriptinthefollowingmanner:
/*util/index.js*/
Logger=require("./logger.js");
exports.logger=newLogger();
/*util/logger.js*/
functionLogger(){
}
Logger.prototype.log=(message/*...*/){
console.log.apply(console,arguments);
};
module.exports=Logger;
/*main.js*/
util=require("./utils");
util.logger.log("Thisisprettycool");
Runningmain.js:
[~/src/examples/example-2]$nodemain.js
Thisisprettycool
Requirecanalsobeusedtoincludemodulesinourcode.Whenrequiringmodules,wedon’tneedtouseafilepath,wejustneedthenameofthenodemodulethatwewant.
Node.jsincludesmanyprebuiltcoremodules,oneofwhichistheutilmodule.Youcanfinddetailsontheutilmoduleathttps://nodejs.org/api/util.html.
Let’sseetheutilmodulecommand:
[~]$node
>varutil=require("util")
>util.log('Thisisprettycoolaswell')
01Jan00:00:00-Thisisprettycoolaswell
HellonpmAlongwithinternalmodulesthereisalsoanentireecosystemofpackages;themostcommonpackagemanagerforNode.jsisnpm.Atthetimeofwriting,thereareatotalof192,875packagesavailable.
Wecanusenpmtoaccesspackagesthatdomanythingsforus,fromroutingHTTPrequeststobuildingourprojects.Youcanalsobrowsethepackagesavailableathttps://www.npmjs.com/.
Usingapackagemanageryoucanbringinothermodules,whichisgreatasyoucanspendmoretimeworkingonyourbusinesslogicratherthanreinventingthewheel.
Let’sdownloadthefollowingpackagetomakeourlogmessagescolorful:
[~/src/examples/example-3]$npminstallchalk
Now,touseit,createafileandrequireit:
[~/src/examples/example-3]$touchindex.js
/*index.js*/
varchalk=require("chalk");
console.log("Iamjustnormaltext")
console.log(chalk.blue("Iambluetext!"))
Onrunningthiscode,youwillseethefirstmessageinadefaultcolorandthesecondmessageinblue.Let’slookatthecommand:.
[~/src/examples/example-3]$nodeindex.js
Iamjustnormaltext
Iambluetext!
Havingtheabilitytodownloadexistingpackagescomesinhandywhenyourequiresomethingthatsomeoneelsehasalreadyimplemented.Aswesaidearlier,therearemanypackagesouttheretochoosefrom.
Weneedtokeeptrackofthesedependenciesandthereisasimplesolutiontothat:package.json.
Usingpackage.jsonwecandefinethings,suchasthenameofourproject,whatthemainscriptis,howtoruntests,ourdependencies,andsoon.Youcanfindafulllistofpropertiesathttps://docs.npmjs.com/files/package.json.
npmprovidesahandycommandtocreatethesefilesanditwillaskyoutherelevantquestionsneededtocreateyourpackage.jsonfile:
[~/src/examples/example-3]$npminit
Theprecedingutilitywillwalkyouthroughthecreationofapackage.jsonfile.
Itonlycoversthemostcommonitemsandtriestoguessvaliddefaults.
Runthenpmhelpjsoncommandfordefinitivedocumentationonthesefieldsandtoknowwhattheydoexactly.
Afterwards,usenpmandinstall<pkg>--savetoinstallapackageandsaveitasadependencyinthepackage.jsonfile.
Press^Ctoquitatanytime:
name:(example-3)
version:(1.0.0)
description:
entrypoint:(main.js)
testcommand:
gitrepository:
keywords:
license:(ISC)
Abouttowriteto/examples/example-3/package.json:
{
"name":"example-3",
"version":"1.0.0",
"description":"",
"main":"main.js",
"scripts":{
"test":"echo\"Error:notestspecified\"&&exit1"
},
"author":"....",
"license":"ISC"
}
Isthisok?(yes)
Theutilitywillprovideyouwithdefaultvalues,soitiseasiertojustskipthroughthemusingtheEnterkey.
Nowwheninstallingourpackagewecanusethe--saveoptiontosavechalkasadependency,asshown:
[~/src/examples/example-3]$npminstall--savechalk
Wecanseechalkhasbeenadded:
[~/examples/example-3]$catpackage.json
{
"name":"example-3",
"version":"1.0.0",
"description":"",
"main":"main.js",
"scripts":{
"test":"echo\"Error:notestspecified\"&&exit1"
},
"author":"...",
"license":"ISC",
"dependencies":{
"chalk":"^1.0.0"
}
}
Wecanaddthesedependenciesmanuallybymodifyingpackage.json;thisisthemostcommonmethodtosavedependenciesoninstallation.
Youcanreadmoreaboutthepackagefileat:https://docs.npmjs.com/files/package.json.
Ifyouarecreatingaserveroranapplicationratherthanamodule,youmostlikelywanttofindawaytostartyourprocesswithouthavingtogiveapathtoyourmainfileallthetime;thisiswherethescriptobjectinyourpackage.jsonfilecomesintoplay.
Tosetyourstartupscript,youjustneedtosetthestartpropertyinthescriptsobject,asshown:
"scripts":{
"test":"echo\"Error:notestspecified\"&&exit1",
"start":"nodeserver.js"
}
Now,allweneedtodoisrunnpmstartandthennpmwillrunthestartscriptwehavealreadyspecified.
Wecandefinemorescripts,forexampleifwewantastartscriptforthedevelopmentenvironmentwecanalsodefineadevelopmentproperty;withnon-standardscriptnameshowever,insteadofjustusingnpm<script>,weneedtousenpmrun<script>.Forexample,ifwewanttorunournewdevelopmentscriptwewillhavetousenpmrundevelopment.
npmhasscriptsthataretriggeredatdifferenttimes.Wecandefineapostinstallscriptthatrunsafterwerunnpminstall;wecanusethisifwewanttotriggerapackagemanagertoinstallthemodules(forexample,bower)
Youcanreadmoreaboutthescriptsobjecthere:https://docs.npmjs.com/misc/scripts.
Youneedtodefineapackageifyouareworkinginateamofdeveloperswheretheprojectistobeinstalledondifferentmachines.Ifyouareusingasourcecontroltoolsuchasgit,itisrecommendedthatyouaddthenode_modulesdirectoryintoyourignorefile,asshown:
[~/examples/example-3]$echo"node_modules">.gitignore
[~/examples/example-3]$cat.gitignore
node_modules
SummaryThatwasquick,wasn’tit?WehavecoveredthefundamentalsofNode.js,whichweneedtocontinueonourjourney.
WehavecoveredhoweasyitistoexposeandprotectpublicandprivatecodecomparedtoregularJavaScriptcodeinthebrowser,wheretheglobalscopecangetverypolluted.
Wealsoknowhowtoincludepackagesandcodefromexternalsourcesandhowtoensurethatthepackagesincludedareconsistent.
Asyoucanseethereisahugeecosystemofpackagesinoneofthemanypackagemanagers,suchasnpm,justwaitingforustouseandconsume.
Inthenextchapter,wewillfocusoncreatingasimpleservertoroute,authenticate,andconsumerequests.
Chapter2.SimpleHTTPNowthatwehaveunderstoodthebasics,wecanmoveontosomethingabitmoreuseful.Inthischapter,wewilllookatcreatinganHTTPserverandroutingrequests.WhileworkingwithNode.jsyouwillcomeacrossHTTPveryoften,asserversidescriptingisoneofthecommonusesofNode.js.
Node.jscomeswithabuiltinHTTPserver;allyouneedtodoisrequiretheincludedhttppackageandcreateaserver.Youcanreadmoreaboutthepackageathttps://nodejs.org/api/http.html.
varHttp=require('http');
varserver=Http.createServer();
ThiswillcreateyourveryownHTTPserverthatisreadytoroll.Inthisstate,though,itwon’tbelisteningforanyrequests.Wecanstartlisteningonanyportorsocketwewish,aslongasitisavailable,asshown:
varHttp=require('http');
varserver=Http.createServer();
server.listen(8080,function(){
console.log('Listeningonport8080');
});
Let’ssavetheprecedingcodetoserver.jsandrunit:
[~/examples/example-4]$nodeserver.js
Listeningonport8080
Bynavigatingtohttp://localhost:8080/onyourbrowseryouwillseethattherequesthasbeenacceptedbuttheserverisn’tresponding;thisisbecausewehaven’thandledtherequestsyet,wearejustlisteningforthem.
Whenwecreatetheserverwecanpassacallbackthatwillbecalledeachtimethereisarequest.Theparameterspassedwillbe:request,response.
functionrequestHandler(request,response){
}
varserver=Http.createServer(requestHandler);
Noweachtimewegetarequestwecandosomething:
varcount=0;
functionrequestHandler(request,response){
varmessage;
count+=1;
response.writeHead(201,{
'Content-Type':'text/plain'
});
message='Visitorcount:'+count;
console.log(message);
response.end(message);
}
Let’srunthescriptandrequestthepagefromthebrowser;youshouldseeVisitorcount:1returnedtothebrowser:
[~/examples/example-4]$nodeserver.js
Listeningonport8080
Visitorcount:1
Visitorcount:2
Somethingweirdhashappenedthough:anextrarequestgetsgenerated.Whoisvisitor2?
Thehttp.IncomingMessage(theparameterrequest)exposesafewpropertiesthatcanbeusedtofigurethisout.Thepropertywearemostinterestedinrightnowisurl.Weareexpectingjust/toberequested,solet’saddthistoourmessage:
message='Visitorcount:'+count+',path:'+request.url;
Nowyoucanrunthecodeandseewhat’sgoingon.Youwillnoticethat/favicon.icohasbeenrequestedaswell.IfyouarenotabletoseethisthenyoumustbewonderingwhatIhavebeengoingonaboutorifyourbrowserhasbeentohttp://localhost:8080recentlyandhasacachediconalready.Ifthisisthecase,thenyoucanrequesttheiconmanually,forexamplefromhttp://localhost:8080/favicon.ico:
[~/examples/example-4]$nodeserver.js
Listeningonport8080
Visitorcount:1,path:/
Visitorcount:2,path:/favicon.ico
Wecanalsoseethatifwerequestanyotherpagewewillgetthecorrectpath,asshown:
[~/examples/example-4]$nodeserver.js
Listeningonport8080
Visitorcount:1,path:/
Visitorcount:2,path:/favicon.ico
Visitorcount:3,path:/test
Visitorcount:4,path:/favicon.ico
Visitorcount:5,path:/foo
Visitorcount:6,path:/favicon.ico
Visitorcount:7,path:/bar
Visitorcount:8,path:/favicon.ico
Visitorcount:9,path:/foo/bar/baz/qux/norf
Visitorcount:10,path:/favicon.ico
Thisisn’tthedesiredoutcomethough,foreverythingbutafewrouteswewanttoreturn404:NotFound.
IntroducingroutingRoutingisessentialforalmostallNode.jsservers.First,wewillimplementourownsimpleversionandthenmoveontothemorecomplexrounting.
Wecanimplementourownsimplerouterusingaswitchstatement,suchas:
functionrequestHandler(request,response){
varmessage,
status=200;
count+=1;
switch(request.url){
case'/count':
message=count.toString();
break;
case'/hello':
message='World';
break;
default:
status=404;
message='NotFound';
break;
}
response.writeHead(201,{
'Content-Type':'text/plain'
});
console.log(request.url,status,message);
response.end(message);
}
Let’srunthefollowingexample:
[~/examples/example-4]$nodeserver.js
Listeningonport8080
/foo404NotFound
/bar404NotFound
/world404NotFound
/count2004
/hello200World
/count2006
Youcanseethecountincreasingwitheachrequest;however,itisn’treturnedeachtime.Ifwehaven’tdefinedacasespecificallyforthatroute,wereturn404:NotFound.
ForservicesthatimplementaRESTfulinterface,wewanttobeabletorouterequestsbasedontheHTTPmethodaswell.Therequestobjectexposesthisusingthemethodproperty.
Addingthistothelogwecanseethis:
console.log(request.method,request.url,status,message);
Runtheexampleandexecuteyourrequests,youcanuseaRESTclienttoinvokeaPOSTrequest:
[~/examples/example-4]$nodeserver.js
Listeningonport8080
GET/count2001
POST/count2002
PUT/count2003
DELETE/count2004
Wecanimplementaroutertoroutebasedonamethod,buttherearepackagesthatdothisforusalreadyoutthere.Fornowwewilluseasimplepackagecalledrouter:
[~/examples/example-5]$npminstallrouter
Now,wecandosomemorecomplexroutingofourrequests:
Let’screateasimpleRESTfulinterface.
First,weneedtocreatetheserver,asshown:
/*server.js*/
varHttp=require('http'),
Router=require('router'),
server,
router;
router=newRouter();
server=Http.createServer(function(request,response){
router(request,response,function(error){
if(!error){
response.writeHead(404);
}else{
//Handleerrors
console.log(error.message,error.stack);
response.writeHead(400);
}
response.end('\n');
});
});
server.listen(8080,function(){
console.log('Listeningonport8080');
});
Runningtheservershouldshowthattheserverislistening.
[~/examples/example-5]$nodeserver.js
Listeningonport8080
Wewanttodefineasimpleinterfacetoread,save,anddeletemessages.Wemightwanttoreadindividualmessagesaswellasalistofmessages;thisessentiallydefinesasetofRESTfulendpoints.
RESTstandsforRepresentationalStateTransfer;itisaverysimpleandcommonstyle
usedbymanyHTTPprogramminginterfaces.
Theendpointswewanttodefineare:
HTTPMethod Endpoint Usedto
POST /message Createmessage
GET /message/:id Readmessage
DELETE /message/:id Deletemessage
GET /message Readmultiplemessages
ForeachHTTPmethod,therouterhasamethodtouseformappingaroute.Thisinterfaceisintheformof:
router.<HTTPmethod>(<path>,[...<handler>])
Wecandefinemultiplehandlersforeachroute,butwewillcomebacktothatinamoment.
Wewillgothrougheachroute,createanimplementation,andappendthecodetotheendofserver.js.
Wewanttostoreourmessagessomewhere,andintherealworldwewillstoretheminadatabase;however,forsimplicitywewilluseanarraywithasimplecounter,asshown:
varcounter=0,
messages={};
Ourfirstroutewillbeusedtocreatemessages:
functioncreateMessage(request,response){
varid=counter+=1;
console.log('Createmessage',id);
response.writeHead(201,{
'Content-Type':'text/plain'
});
response.end('Message'+id);
}
router.post('/message',createMessage);
WecanensurethatthisrouteworksbyrunningtheserveranddoingaPOSTrequesttohttp://localhost:8000/message.
[~/examples/example-5]$nodeserver.js
Listeningonport8080
Createmessage1
Createmessage2
Createmessage3
Wecanalsoconfirmthatthecounterisincrementing,astheidincreaseseachtimewemakearequest.Wewilldothistokeepatrackofthecountofmessagesandtogiveauniqueidtoeachmessage.
Nowthatthisisworking,weneedtobeabletoreadthemessagetextandtodothisweneedtobeabletoreadtherequestbodythatwassentbytheclient.Thisiswheremultiplehandlerscomeintoplay.Wecouldtacklethisintwodifferentways,ifwewerereadingthebodyinonlyonerouteorifweweredoingsomeotheractionspecifictoaroute,forinstanceauthorization,wewilladdanadditionalhandlertotheroute,suchas:
router.post('/message',parseBody,createMessage)
Theotherwaywecoulddoitisbyaddingahandlerforallmethodsandroutes;thiswillbeexecutedfirstbeforetheroutehandlers,thesearecommonlyreferredtoasmiddleware.Youcanthinkofhandlersasbeingachainoffunctionswhereeachoneiscallingthenext,onceitisfinishedwithitstasks.Withthisinmindyoushouldnotethattheorderinwhichyouaddahandler,bothmiddlewareandroute,willdictatetheorderofoperations.Thismeansthat,ifweareregisteringahandlerthatisexecutedforallmethods,wemustdothisfirst.
Therouterexposesafunctiontoaddthefollowinghandlers:
router.use(function(request,response,next){
console.log('middlewareexecuted');
//Nullastherewerenoerrors
//Iftherewasanerrorthenwecouldcall`next(error);`
next(null);
});
YoucanaddthiscodejustaboveyourimplementationofcreateMessage:
Onceyouhavedonethat,runtheserverandmakethefollowingrequest:
[~/examples/example-5]$nodeserver.js
Listeningonport8080
middlewareexecuted
Createmessage1
Youcanseethatthemiddlewaregetsexecutedbeforetheroutehandler.
Nowthatweknowhowmiddlewareworks,wecanusethemasfollows:
[~/examples/example-5]$npminstallbody-parser
Replaceourcustommiddlewarewith:
varBodyParser=require('body-parser');
router.use(BodyParser.text());
Atthisstage,wejustwanttoreadallrequestsasplaintext.
NowwecanretrievethemessageincreateMessage:
functioncreateMessage(request,response){
varid=counter+=1,
message=request.body;
console.log('Createmessage',id,message);
messages[id]=message;
response.writeHead(201,{
'Content-Type':'text/plain',
'Location':'/message/'+id
});
response.end(message);
}
Runserver.jsandPOSTacoupleofmessagestohttp://localhost:8080/message;youwillseesomethingsimilartothesemessages:
[~/examples/example-5]$nodeserver.js
Listeningonport8080
Createmessage1Hellofoo
Createmessage2Hellobar
Ifyounotice,youwillseethataheaderreturnswithanewlocationofthemessageanditsid,Ifwerequesthttp://localhost:8080/message/1,thecontentfromthefirstmessageshouldbereturned.
However,thereissomethingdifferentwiththisroute;ithasakeythatisgeneratedeachtimeamessageiscreated.Wedon’twanttosetupanewrouteforeachnewmessageasitwillbehighlyinefficient.Instead,wecreatearoutethatmatchesapattern,suchas/message/:id.ThisisacommonwaytodefineadynamicrouteinNode.js.
Theidpartoftherouteiscalledaparameter.Wecandefineasmanyoftheseaswewantinourrouteandreferthemusingtherequest;forexamplewecanhavearoutesimilarto/user/:id/profile/:attribute.
WiththisinmindwecancreateourreadMessagehandler,asshown:
functionreadMessage(request,response){
varid=request.params.id,
message=messages[id];
console.log('Readmessage',id,message);
response.writeHead(200,{
'Content-Type':'text/plain'
});
response.end(message);
}
router.get('/message/:id',readMessage);
Nowlet’ssavetheprecedingcodeintheserver.jsfileandruntheserver:
[~/examples/example-5]$nodeserver.js
Listeningonport8080
Createmessage1Hellofoo
Readmessage1Hellofoo
Createmessage2Hellobar
Readmessage2Hellobar
Readmessage1Hellofoo
Wecanseeit’sworkingbysendingafewrequeststotheserver.
Deletingmessagesisalmostthesameasreadingthem;butwedon’treturnanythingandnullouttheoriginalmessagevalue:
functiondeleteMessage(request,response){
varid=request.params.id;
console.log('Deletemessage',id);
messages[id]=undefined;
response.writeHead(204,{});
response.end('');
}
router.delete('/message/:id',deleteMessage)
First,runtheserver,thencreate,read,anddeleteamessage,asshown:
[~/examples/example-5]$nodeserver.js
Listeningonport8080
Deletemessage1
Createmessage1Hello
Readmessage1Hello
Deletemessage1
Readmessage1undefined
Thatlooksgood;however,wehaverunintoaproblem.Weshouldn’tbeabletoreadamessageagainafterdeletingit;wewillreturn404inboththereadanddeletehandlersifwecan’tfindamessage.Wecandothisbyaddingthefollowingcodetoourreadanddeletehandlers:
varid=request.params.id,
message=messages[id];
if(typeofmessage!=='string'){
console.log('Messagenotfound',id);
response.writeHead(404);
response.end('\n');
return;
}
Nowlet’ssavetheprecedingcodeintheserver.jsfileandruntheserver:
[~/examples/example-5]$nodeserver.js
Listeningonport8080
Messagenotfound1
Createmessage1Hello
Readmessage1Hello
Lastly,wewanttobeabletoreadallmessagesandreturnalistofallmessagevalues:
functionreadMessages(request,response){
varid,
message,
messageList=[],
messageString;
for(idinmessages){
if(!messages.hasOwnProperty(id)){
continue;
}
message=messages[id];
//Handledeletedmessages
if(typeofmessage!=='string'){
continue;
}
messageList.push(message);
}
console.log('Readmessages',JSON.stringify(
messageList,
null,
''
));
messageString=messageList.join('\n');
response.writeHead(200,{
'Content-Type':'text/plain'
});
response.end(messageString);
}
router.get('/message',readMessages);
Nowlet’ssavetheprecedingcodeintheserver.jsfileandruntheserver:
[~/examples/example-5]$nodeserver.js
Listeningonport8080
Createmessage1Hello1
Createmessage2Hello2
Createmessage3Hello3
Createmessage4Hello4
Createmessage5Hello5
Readmessages[
"Hello1",
"Hello2",
"Hello3",
"Hello4",
"Hello5"
]
Awesome;nowwehaveafullRESTfulinterfacetoreadandwritemessages.But,wedon’twanteveryonetobeabletoreadourmessages;theyshouldbesecureandwealsowanttoknowwhoiscreatingthemessages,wewillcoverthisinthenextchapter.
SummaryNowwehaveeverythingweneedtomakesomeprettycoolservices.WecannowcreateanHTTPfromscratch,routeourrequests,andcreateaRESTfulinterface.
ThiswillhelpyouwiththecreationofcompleteNode.JSservices.Inthenextchapter,wewillcoverauthentication.
Chapter3.AuthenticationWecannowcreateRESTfulAPIs,butwedon’twanteveryonetoaccesseverythingweexpose.Wewanttheroutestobesecureandtobeabletotrackwhoisdoingwhat.
Passportisagreatmoduleandanothermiddlewarethathelpsusauthenticaterequests.
PassportexposesasimpleAPIforproviderstoexpandonandcreatestrategiestoauthenticateusers.Atthetimeofwriting,thereare307officiallysupportedstrategies;however,thereisnoreasonwhyyoucan’twriteyourownstrategyandpublishitforotherstouse.
BasicauthenticationThesimpleststrategyforpassportisthelocalstrategythatacceptsausernameandpassword.
Wewillintroducetheexpressframeworkfortheseexamplesand,nowthatyouknowthebasicsofhowitallworksunderneath,wecanputitalltogether.
Youcaninstallexpress,body-parser,passport,andpassport-local.Expressisabatteries-includedWebframeworkforNode.js,andincludesroutingandtheabilitytousemiddleware:
[~/examples/example-19]$npminstallexpressbody-parserpassportpassport-
local
Fornow,wecanstoreourusersinasimpleobjecttoreferencelater,asshown:
varusers={
foo:{
username:'foo',
password:'bar',
id:1
},
bar:{
username:'bar',
password:'foo',
id:2
}
}
Oncewehaveafewusers,weneedtosetuppassport.Whenwecreateaninstanceofthelocalstrategy,weneedtoprovideaverifycallbackwherewechecktheusernameandpassword,whilereturningauser:
varPassport=require('passport'),
LocalStrategy=require('passport-local').Strategy;
varlocalStrategy=newLocalStrategy({
usernameField:'username',
passwordField:'password'
},
function(username,password,done){
user=users[username];
if(user==null){
returndone(null,false,{message:'Invaliduser'});
}
if(user.password!==password){
returndone(null,false,{message:'Invalidpassword'});
}
done(null,user);
}
)
Theverifycallbackinthiscaseisexpectingdonetobecalledwithauser.Italsoallowsustoprovideinformationiftheuserwasinvalidorthepasswordwaswrong.
Now,thatwehaveastrategywecanpassthistopassport,whichallowsustoreferenceitlateranduseittoauthenticateourrequests,asfollows:
Passport.use('local',localStrategy);
Youcanusemultiplestrategiesperapplicationandreferenceeachonebythenameyoupassed,inthiscase'local'.
Now,let’screateourserver,asshownhere:
varExpress=require('express');
varapp=Express();
Wewillhavetousethebody-parsermiddleware.Thiswillensurethat,whenweposttoourloginroute,wecanreadourbody;wealsoneedtoinitializepassport:
varBodyParser=require('body-parser');
app.use(BodyParser.urlencoded({extended:false}));
app.use(BodyParser.json());
app.use(Passport.initialize());
Tologintoourapplication,weneedtocreateapostroutethatusesauthenticationasoneofthehandlers.Thecodeforthisisasfollows:
app.post(
'/login',
Passport.authenticate('local',{session:false}),
function(request,response){
}
);
Now,whenwesendaPOSTrequestto/logintheserverwillauthenticateourrequests.
Onceauthenticated,theuserpropertywillbepopulatedontherequestobject,asfollows:
app.post(
'/login',
Passport.authenticate('local',{session:false}),
function(request,response){
response.send('UserId'+request.user.id);
}
);
Lastly,weneedtolistenforrequests,aswithalltheotherservers:
app.listen(8080,function(){
console.log('Listeningonport8080');
});
Letsruntheexample:
[~/examples/example-19]$nodeserver.js
Listeningonport8080
Now,wecanauthenticateuserswhenwesendaPOSTrequestatourserver.Iftheuserhasn’tpassedboththeusernameandpasswordtheserverwillreturn400BadRequest.
TipIfyouaren’tfamiliarwithcurlyoucoulduseatool,suchasAdvancedRESTClient:
https://chromerestclient.appspot.com/
InthefollowingexamplesIwillbeusingthecommandlineinterfacecurl.
WecanexecutealoginrequestbyexecutingaPOSTto/logincommand:
[~]$curl-XPOSThttp://localhost:8080/login-v
<HTTP/1.1400BadRequest
Iftheuserprovidesthewrongdetailsthen401Unauthorizedwillbereturned:
[~]$curl-XPOSThttp://localhost:8080/login\
-H'Content-Type:application/json'\
-d'{"username":"foo","password":"foo"}'\
-v
<HTTP/1.1401Unauthorized
Ifweprovidethecorrectdetailsthenwecanseeourhandlerwascalledandthecorrectdatawasreturned:
[~]$curl-XPOSThttp://localhost:8080/login\
-H'Content-Type:application/json'\
-d'{"username":"foo","password":"bar"}'
UserId1
[~]$curl-XPOSThttp://localhost:8080/login\
-H'Content-Type:application/json'\
-d'{"username":"bar","password":"foo"}'
UserId2
BearertokensNowthatwehaveanauthenticateduser,wecangenerateatokenthatcanbeusedwiththerestofourrequestsratherthanpassingourusernameandpasswordeverywhere.ThisiscommonlyknownasaBearertokenand,conveniently,thereisapassportstrategyforthis.
Forourtokens,wewillusesomethingcalledaJSONWebToken(JWT).JWTallowsustoencodetokensfromJSONobjectsandthendecodethemandverifythem.Thedatastoredinthemisopenandsimpletoread,sopasswordsshouldn’tbestoredinthem;however,itmakesverifyingauserverysimple.Wecanalsoprovidethesetokenswithexpirydates,whichhelpslimittheseverityoftokensbeingexposed.
YoucanreadmoreaboutJWTathttp://jwt.io/.
WecaninstallJWTusingthefollowingcommand:
[~/examples/example-19]$npminstalljsonwebtoken
Onceauserisauthenticated,wecansafelyprovidethemwithatokentouseinfuturerequests:
varJSONWebToken=require('jsonwebtoken'),
Crypto=require('crypto');
vargenerateToken=function(request,response){
//Thepayloadjustcontainstheidoftheuser
//andtheirusername,wecanverifywhethertheclaim
//iscorrectusingJSONWebToken.verify
varpayload={
id:user.id,
username:user.username
};
//Generatearandomstring
//Usuallythiswouldbeanappwideconstant
//Butcanbedonebothways
varsecret=Crypto.randomBytes(128)
.toString('base64');
//Createthetokenwithapayloadandsecret
vartoken=JSONWebToken.sign(payload,secret);
//Theuserisstillreferencingthesameobject
//inusers,sononeedtosetitagain
//Ifwewereusingadatabase,wewouldsave
//ithere
request.user.secret=secret
returntoken;
}
vargenerateTokenHandler=function(request,response){
varuser=request.user;
//Generateourtoken
vartoken=generateToken(user);
//Returntheuseratokentouse
response.send(token);
};
app.post(
'/login',
Passport.authenticate('local',{session:false}),
generateTokenHandler
);
Now,whentheuserlogsintheywillbepresentedwithatokentousethatwecanverify.
LetsrunourNode.jsserver:
[~/examples/example-19]$nodeserver.js
Listeningonport8080
Whenweloginnowwereceiveatoken:
[~]$curl-XPOSThttp://localhost:8080/login\
-H'Content-Type:application/json'\
-d'{"username":"foo","password":"bar"}'
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZC
I6MSwidXNlcm5hbWUiOiJmb28iLCJpYXQiOjE0MzcyO
TQ3OTV9.iOZO7oCIceZl6YvZqVP9WZLRx-XVvJFMF1p
pPCEsGGs
Wecanenterthisintothedebuggerathttp://jwt.io/andseethecontents,asshown:
{
"id":1,
"username":"foo",
"iat":1437294795
}
Ifwehadthesecretwecouldverifythatthetokeniscorrect.Thesignaturechangeseverytimewerequestatoken:
[~]$curl-XPOSThttp://localhost:8080/login\
-H'Content-Type:application/json'\
-d'{"username":"foo","password":"bar"}'
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZC
I6MSwidXNlcm5hbWUiOiJmb28iLCJpYXQiOjE0MzcyO
TQ5OTl9.n1eRQVOM9qORTIMUpslH-ycTNEYdLDKa9lU
pmhf44s0
Wecanauthenticateauserusingpassport-bearer;itissetupverysimilartopassport-local.However,ratherthanacceptingausernameandpasswordfromthebody,weacceptabearertoken;thiscanbepassedusingthequerystring,body,ortheAuthorizationheader:
Firstwemustinstallpassport-http-bearer:
[~/examples/example-19]$npminstallpassport-http-bearer
Thelet’screateourverifier.Therearetwosteps:thefirstisensuringthedecodedinformationmatchesouruser,thiswillbewhereweusuallyretrieveouruser;then’once
wehaveauserandit’svalid,wecancheckwhetherthetokenisvalidbasedontheuser’ssecret:
varBearerStrategy=require('passport-http-bearer').Strategy;
varverifyToken=function(token,done){
varpayload=JSONWebToken.decode(token);
varuser=users[payload.username];
//Ifwecan'tfindauser,ortheinformation
//doesn'tmatchthenreturnfalse
if(user==null||
user.id!==payload.id||
user.username!==payload.username){
returndone(null,false);
}
//Ensurethetokenisvalidnowwehaveauser
JSONWebToken.verify(token,user.secret,function(error,decoded){
if(error||decoded==null){
returndone(error,false);
}
returndone(null,user);
});
}
varbearerStrategy=newBearerStrategy(
verifyToken
)
Wecanregisterthisstrategyasthebearersowecanuseitlater:
Passport.use('bearer',bearerStrategy);
Wecancreateasimpleroutewhereweretrieveuserdetailsforanauthenticateduser:
app.get(
'/userinfo',
Passport.authenticate('bearer',{session:false}),
function(request,response){
varuser=request.user;
response.send({
id:user.id,
username:user.username
});
}
);
Let’sruntheNode.jsserver:
[~/examples/example-19]$nodeserver.js
Listeningonport8080
Oncewereceiveatoken:
[~]$curl-XPOSThttp://localhost:8080/login\
-H'Content-Type:application/json'\
-d'{"username":"foo","password":"bar"}'
Wecanusetheresultinourrequests:
[~]$curl-XGEThttp://localhost:8080/userinfo\
-H'Authorization:Bearer<token>'
{"id":1,"username":"foo"}
OAuthOAuthprovidesmanyadvantages;forinstance,itdoesnotneedtodealwiththeactualidentificationofusers.Wecanletusersloginusingservicestheytrust,suchasGoogle,Facebook,orAuth0.
Forthefollowingexamples,IwillbeusingAuth0.Theyprovideafreeaccountforyoutogetup-and-running:https://auth0.com/.
Youwillneedtosignupandcreateanapi(chooseAngularJS+Node.js),thengotoSettingsandtakedownthedomain,clientid,andclientsecret.YouwillneedthesetosetupOAuth.
WecanauthenticateusingOAuthusingpassport-oauth2:
[~/examples/example-19]$npminstall--savepassport-oauth2
Aswithourbearertokens,wewanttovalidatewhattheserverreturns,whichwillbeauserobjectthathasanid.Wewillmatchthiswithauserthatisinourdataorcreateanewuser:
varvalidateOAuth=function(accessToken,refreshToken,profile,done){
varkeys=Object.keys(users),user=null;
for(variKey=0;iKey<keys.length;i+=1){
user=users[key];
if(user.thirdPartyId!==profile.user_id){continue;}
returndone(null,user);
}
users[profile.name]=user={
username:profile.name,
id:keys.length,
thirdPartyId:profile.user_id
}
done(null,user);
};
OncewehaveafunctiontovalidateouruserswecanputtogethertheoptionsforourOAuthstrategy:
varoAuthOptions={
authorizationURL:'https://<domain>.auth0.com/authorize',
tokenURL:'https://<domain>.auth0.com/oauth/token',
clientID:'<clientid>',
clientSecret:'<clientsecret>',
callbackURL:"http://localhost:8080/oauth/callback"
}
Thenwecreateourstrategy,asfollows:
varOAuth2Strategy=require('passport-oauth2').Strategy;
oAuthStrategy=newOAuth2Strategy(oAuthOptions,validateOAuth);
BeforeweuseourstrategyweneedtoducktypethestrategiesuserProfilemethodwithourown,thisissowecanrequesttheuserobjecttouseinvalidateOAuth:
varparseUserProfile=function(done,error,body){
if(error){
returndone(newError('Failedtofetchuserprofile'))
}
varjson;
try{
json=JSON.parse(body);
}catch(error){
returndone(error);
}
done(null,json);
}
vargetUserProfile=function(accessToken,done){
oAuthStrategy._oauth2.get(
"https://<domain>.auth0.com/userinfo",
accessToken,
parseUserProfile.bind(null,done)
)
}
oAuthStrategy.userProfile=getUserProfile
Wecanregisterthisstrategyasoauthsowecanuseitlater:
Passport.use('oauth',oAuthStrategy);
WeneedtocreatetworoutestohandleourOAuthauthentication:oneroutetostarttheflowandtheotherfortheidentificationservertoreturnto:
app.get('/oauth',Passport.authenticate('oauth',{session:false}));
WecanuseourgenerateTokenHandlerhere,asourrequestwillhaveauseronit.
app.get('/oauth/callback',
Passport.authenticate('oauth',{session:false}),
generateTokenHandler
);
Wecannowstartourserverandrequesthttp://localhost:8080/oauth;theserverwillredirectyoutoAuth0.Onceloggedin,youwillreceiveatokenthatyoucanusewith/userinfo.
Ifyouwereusingsessions,youcouldsavetheusertothesessionandredirectthembacktoyourfrontpage(orthedefaultpagesetforaloggedinuser).Forasingle-pageapp,whenusingsomethinglikeAngular,youmaywanttoredirecttheuserwithatokenintheURLfortheclientframeworktograbontoandsave.
SummaryWecannowauthenticateusers;thisisgreataswecannowfigureoutwhothepeopleareandthenlimittheuserstocertainresources.
Inthenextchapterwewillcoverdebugging,wemayneedtouseitifourusersaren’tbeingauthenticated.
Chapter4.DebuggingAtsomepointinyourjourneywithNode.js,itisinevitablethatyouwillhavetodebugsomenastybugs.So,let’sexpectthembeforehandandplanforthatday.
LoggingThereareafewmethodsthatwecanusetodebugoursoftware;thefirstonewearegoingtolookatislogging.Thesimplestwaytologamessageistouseconsole.InmostofthepreviousexamplesconsolehasbeenusedtoportraywhatisgoingonwithoutneedingtoseetheentireHTTPrequestandresponse,thusmakingthingsalotmorereadableandsimple.
Anexampleofthisis:
varHttp=require('http');
Http.createServer(function(request,response){
console.log(
'Receivedrequest',
request.method,
request.url
)
console.log('Returning200');
response.writeHead(200,{'Content-Type':'text/plain'});
response.end('HelloWorld\n');
}).listen(8000);
console.log('Serverrunningonport8000');
Runningthisexamplewilllogrequestsandresponsesontheconsole:
[~/examples/example-6]$nodeserver.js
Serverrunningonport8000
ReceivedrequestGET/
Returning200
ReceivedrequestGET/favicon.ico
Returning200
ReceivedrequestGET/test
Returning200
ReceivedrequestGET/favicon.ico
Returning200
Ifweareusingaframeworkthatacceptsmiddleware,suchasexpress,wecoulduseasimplenpmpackagecalledmorgan;youcanfindthepackageathttps://www.npmjs.com/package/morgan:
[~/examples/example-7]$npminstallmorgan
[~/examples/example-7]$npminstallrouter
Wecanuseitbyusingrequiretobringitintoourcodeandaddingitasmiddleware:
varMorgan=require('morgan'),
Router=require('router'),
Http=require('http');
router=newRouter();
router.use(Morgan('tiny'));
/*Simpleserver*/
Http.createServer(function(request,response){
router(request,response,function(error){
if(!error){
response.writeHead(404);
}else{
//Handleerrors
console.log(error.message,error.stack);
response.writeHead(400);
}
response.end('\n');
});
}).listen(8000);
console.log('Serverrunningonport8000');
functiongetInfo(request,response){
varinfo=process.versions;
info=JSON.stringify(info);
response.writeHead(200,{'Content-Type':'application/json'});
response.end(info);
}
router.get('/info',getInfo);
Whentheserverisrunning,wecanseeeachrequestandresponsewithouthavingtoaddloggingtoeachhandler:
[~/examples/example-7]$nodeserver.js
Serverrunningonport8000
GET/test404--4.492ms
GET/favicon.ico404--2.281ms
GET/info200--1.120ms
GET/info200--1.120ms
GET/test404--0.199ms
GET/info200--0.494ms
GET/test404--0.162ms
Thiskindofloggingisasimplewaytoseewhatisbeingusedontheserverandhowlongeachrequestistaking.Here,youcanseethatthefirstrequeststookthelongestandthentheygotalotfaster.Thedifferenceisonlyof3ms;ifthetimewaslarger,itcouldhavebeenabigproblem.
Wecanincreasetheinformationthat’sloggedbychangingtheformatwepasstomorgan,asshown:
router.use(Morgan('combined'));
Byrunningtheserveryouwillseemoreinformation,suchastheremoteuser,dateandtimeoftherequest,amountofcontentthatwasreturned,andtheclienttheyareusing.
[~/examples/example-7]$nodeserver.js
Serverrunningonport8000
::1--[07/Jun/2015:11:09:03+0000]"GET/infoHTTP/1.1"200-"-""--
REMOVED---"
Timingisdefinitelyanimportantfactorasitcanbehelpfulwhensiftingthroughthemountainsoflogsthatyouwillobtain.Somebugscanbelikeatickingtime-bombwaitingtoexplodeat3AMonaSaturdaynight.Alltheselogsmeannothingtousiftheprocesshasdiedandthelogshavedisappeared.Thereisanotherpopularandusefulpackagecalledbunyan,whichwrapsmanyloggingmethodsintoone.
Bunyanbringstothetabletheadvantageofwriteablestreamstowritelogs,whetheritisafileondiskorstdout.Thisallowsustopersistourlogsforpostmortemdebugging.Youcanfindmoredetailsaboutbunyanathttps://www.npmjs.com/package/bunyan.
Now,let’sinstallthepackage.Wewantitinstalledbothlocallyandgloballysothatwecanalsouseitasacommandlinetool:
[~/examples/example-8]$npminstall–gbunyan
[~/examples/example-8]$npminstallbunyan
Now,letsdosomelogging:
varBunyan=require('bunyan'),
logger;
logger=Bunyan.createLogger({
name:'example-8'
});
logger.info('Hellologging');
Runningourexample:
[~/examples/example-8]$nodeindex.js
{"name":"example-
8","hostname":"macbook.local","pid":2483,"level":30,"msg":"Hello
logging","time":"2015-06-07T11:35:13.973Z","v":0}
Thisdoesn’tlookverypretty,doesit?BunyanusesasimplestructuredJSONstringtosavemessages;thismakesiteasytoparse,extend,andread.BunyancomeswithaCLIutilitytomakeeverythingniceandpretty.
Ifweruntheexamplewiththeutility,thenwewillseethattheoutputisnicelyformatted:
[~/examples/example-8]$nodeindex.js|bunyan
[2015-06-07T11:38:59.698Z]INFO:example-8/2494onmacbook.local:Hello
logging
Ifweaddafewmorelevels,youwillseeonyourconsolethateachiscoloreddifferentlytohelpusidentifythem:
varBunyan=require('bunyan'),
logger;
logger=Bunyan.createLogger({
name:'example-8'
});
logger.trace('Trace');
logger.debug('Debug');
logger.info('Info');
logger.warn('Warn');
logger.error('Error');
logger.fatal('Fatal');
logger.fatal('Wegotafatal,letsexit');
process.exit(1);
Let’sruntheexample:
[~/examples/example-8]$nodeindex.js|bunyan
[2015-06-07T11:39:55.801Z]INFO:example-8/2512onmacbook.local:Info
[2015-06-07T11:39:55.811Z]WARN:example-8/2512onmacbook.local:Warn
[2015-06-07T11:39:55.814Z]ERROR:example-8/2512onmacbook.local:Error
[2015-06-07T11:39:55.814Z]FATAL:example-8/2512onmacbook.local:Fatal
[2015-06-07T11:39:55.814Z]FATAL:example-8/2512onmacbook.local:Wegota
fatal,letsexit
Ifyounotice,traceanddebugweren’toutputtedontheconsole.Thisisbecausetheyareusedtofollowtheflowoftheprogramratherthanthekeyinformationandareusuallyverynoisy.
Wecanchangetheleveloflogswewanttoseebypassingthisasanoptionwhenwecreatethelogger:
logger=Bunyan.createLogger({
name:'example-8',
level:Bunyan.TRACE
});
Now,whenweruntheexample:
[~/examples/example-8]$nodeindex.js|bunyan
[2015-06-07T11:55:40.175Z]TRACE:example-8/2621onmacbook.local:Trace
[2015-06-07T11:55:40.177Z]DEBUG:example-8/2621onmacbook.local:Debug
[2015-06-07T11:55:40.178Z]INFO:example-8/2621onmacbook.local:Info
[2015-06-07T11:55:40.178Z]WARN:example-8/2621onmacbook.local:Warn
[2015-06-07T11:55:40.178Z]ERROR:example-8/2621onmacbook.local:Error
[2015-06-07T11:55:40.178Z]FATAL:example-8/2621onmacbook.local:Fatal
[2015-06-07T11:55:40.178Z]FATAL:example-8/2621onmacbook.local:Wegota
fatal,letsexit
Weusuallydon’twanttoseelogsthatarelowerthantheinfolevel,asanyinformationthatisusefulforpost-mortemdebuggingshouldhavebeenloggedusingtheinfoorhigher.
Bunyan’sapiisgoodforthefunctionofloggingerrorsandobjects.ItsavesthecorrectstructuresinitsJSONoutput,whichisreadyfordisplay:
try{
ref.go();
}catch(error){
logger.error(error);
}
Let’sruntheexample:
[~/examples/example-9]$nodeindex.js|bunyan
[2015-06-07T12:00:38.700Z]ERROR:example-9/2635onmacbook.local:refis
notdefined
ReferenceError:refisnotdefined
atObject.<anonymous>(~/examples/example-8/index.js:9:2)
atModule._compile(module.js:460:26)
atObject.Module._extensions..js(module.js:478:10)
atModule.load(module.js:355:32)
atFunction.Module._load(module.js:310:12)
atFunction.Module.runMain(module.js:501:10)
atstartup(node.js:129:16)
atnode.js:814:3
Ifwelookattheexampleandpretty-printit,wewillseethattheysaveitasanerror:
[~/examples/example-9]$npminstall-gprettyjson
[~/examples/example-9]$nodeindex.js|prettyjson
name:example-9
hostname:macbook.local
pid:2650
level:50
err:
message:refisnotdefined
name:ReferenceError
stack:
"""
ReferenceError:refisnotdefined
atObject.<anonymous>(~/examples/example-8/index.js:9:2)
atModule._compile(module.js:460:26)
atObject.Module._extensions..js(module.js:478:10)
atModule.load(module.js:355:32)
atFunction.Module._load(module.js:310:12)
atFunction.Module.runMain(module.js:501:10)
atstartup(node.js:129:16)
atnode.js:814:3
"""
msg:refisnotdefined
time:2015-06-07T12:02:33.875Z
v:0
Thisisusefulbecause,ifyoujustloganerror,youwilleithergetanemptyobjectifyouusedJSON.stringifyorjustthemessageifyouusedtoString:
try{
ref.go();
}catch(error){
console.log(JSON.stringify(error));
console.log(error);
console.log({
message:error.message
name:error.name
stack:error.stack
});
}
Let’sruntheexample:
[~/examples/example-10]$nodeindex.js
{}
[ReferenceError:refisnotdefined]
{message:'refisnotdefined',
name:'ReferenceError',
stack:'--REMOVED--'}
Itistolotsimplerandcleanertouselogger.error(error)thanlogger.error({message:error.message/*,...*/});.
Asmentionedearlier,bunyanusestheconceptofstreams,whichmeansthatwecanwritetoafile,stdout,oranyotherservicewewishtoextendto.
Towritetoafile,allweneedtodoisaddittotheoptionspassedtobunyanatsetup:
varBunyan=require('bunyan'),
logger;
logger=Bunyan.createLogger({
name:'example-11',
streams:[
{
level:Bunyan.INFO,
path:'./log.log'
}
]
});
logger.info(process.versions);
logger.info('Applicationstarted');
Byrunningtheexample,youwon’tseeanylogsbeingoutputtedtotheconsolebuttheywillbewrittentofileinstead:
[~/examples/example-11]$nodeindex.js
Ifyoulistwhat’sinthedirectoryyouwillseeanewfilehasbeencreated:
[~/examples/example-11]$ls
index.jslog.lognode_modules
Ifyoureadwhat’sinthefileyouwillseethatthelogshavealreadybeenwritten:
[~/examples/example-11]$catlog.log
{"name":"example-
11","hostname":"macbook.local","pid":3614,"level":30,"http_parser":"2.3","n
ode":"0.12.2","v8":"3.28.73","uv":"1.4.2-
node1","zlib":"1.2.8","modules":"14","openssl":"1.0.1m","msg":"","time":"20
15-06-07T12:29:46.606Z","v":0}
{"name":"example-
11","hostname":"macbook.local","pid":3614,"level":30,"msg":"Application
started","time":"2015-06-07T12:29:46.608Z","v":0}
Wecanrunthisthroughbunyaninordertoprintitoutnicely:
[~/examples/example-11]$catlog.log|bunyan
[~/examples/example-11]$catlog.log|bunyan
[2015-06-07T12:29:46.606Z]INFO:example-11/3614onmacbook.local:
(http_parser=2.3,node=0.12.2,v8=3.28.73,uv=1.4.2-node1,zlib=1.2.8,
modules=14,openssl=1.0.1m)
[2015-06-07T12:29:46.608Z]INFO:example-11/3614onmacbook.local:
Applicationstarted
Nowthatwecanlogtoafile,wealsowanttobeabletoseethemessagesastheyaredisplayed.Ifwewerejustloggingtoafile,wecoulduse:
[~/examples/example-11]$tail-flog.log|bunyan
Thiswilllogtostdoutasthefileitisbeingwrittento;alternativelywecouldjustaddanotherstreamtobunyan:
logger=Bunyan.createLogger({
name:'example-11',
streams:[
{
level:Bunyan.INFO,
path:'./log.log'
},
{
level:Bunyan.INFO,
stream:process.stdout
}
]
});
Runningtheexamplewilldisplaythelogstotheconsole:
[~/examples/example-11]$nodeindex.js|bunyan
[2015-06-07T12:37:19.857Z]INFO:example-11/3695onmacbook.local:
(http_parser=2.3,node=0.12.2,v8=3.28.73,uv=1.4.2-node1,zlib=1.2.8,
modules=14,openssl=1.0.1m)[2015-06-07T12:37:19.860Z]INFO:example-
11/3695onmacbook.local:Applicationstarted
Wecanalsoseethelogshavebeenappendedtothefile:
[~/examples/example-11]$catlog.log|bunyan
[2015-06-07T12:29:46.606Z]INFO:example-11/3614onmacbook.local:
(http_parser=2.3,node=0.12.2,v8=3.28.73,uv=1.4.2-node1,zlib=1.2.8,
modules=14,openssl=1.0.1m)
[2015-06-07T12:29:46.608Z]INFO:example-11/3614onmacbook.local:
Applicationstarted
[2015-06-07T12:37:19.857Z]INFO:example-11/3695onmacbook.local:
(http_parser=2.3,node=0.12.2,v8=3.28.73,uv=1.4.2-node1,zlib=1.2.8,
modules=14,openssl=1.0.1m)
[2015-06-07T12:37:19.860Z]INFO:example-11/3695onmacbook.local:
Applicationstarted
Great,nowwehavetheloggingdown,whatshallwedowithit?
Well,ithelpstoknowwhereourerrorsareoccurringanditstartstogetreallymessywhenyouhavelotsofanonymousfunctionsaroundtheplace.IfyounoticedintheexamplesthatcoveranHTTPserver,themajorityofthefunctionswerenamed.Thisisveryhelpfulintrackingdownerrorswhencallbacksareinvolved.
Let’slookatthisexample:
try{
a=function(callback){
returnfunction(){
callback();
};
};
b=function(callback){
returnfunction(){
callback();
}
};
c=function(callback){
returnfunction(){
thrownewError("I'mjustmessingwithyou");
};
};
a(b(c()))();
}catch(error){
logger.error(error);
}
Itmightlookabitmessyandthat’sbecauseitis.Let’srunthefollowingexample:
[~/examples/example-12]$nodeindex.js|bunyan
[2015-06-07T12:51:11.665Z]ERROR:example-12/4158onmacbook.local:I'm
justmessingwithyou
Error:I'mjustmessingwithyou
at/Users/fabian/examples/example-12/index.js:19:10
at/Users/fabian/examples/example-12/index.js:14:4
at/Users/fabian/examples/example-12/index.js:9:4
atObject.<anonymous>(/Users/fabian/examples/example-
12/index.js:22:16)
atModule._compile(module.js:460:26)
atObject.Module._extensions..js(module.js:478:10)
atModule.load(module.js:355:32)
atFunction.Module._load(module.js:310:12)
atFunction.Module.runMain(module.js:501:10)
atstartup(node.js:129:16)
Youcanseethattherearenofunctionnamesinourcodeandalsothereisnonaminginthestacktraceunlikethefirstfewfunctions.InNode.js,thenamingoffunctionswillcomefromeitherthevariablenameortheactualfunctionname.Forexample,ifyouuseCls.prototype.functhenthenamewillbeCls.funcbutifyouusethefunctionfuncthenthenamewillbefunc.
Youcanseethatthereisaslightbenefitherebutthisbecomesveryusefulonceyoustartusingpatternsinvolvingasynccallbacks:
[~/examples/example-13]$npminstallq
Let’sthrowanerrorinacallback:
varQ=require('q');
Q()
.then(function(){
//Promisedreturnedfromanotherfunction
returnQ()
.then(function(){
thrownewError('Helloerrors');
});
})
.fail(function(error){
logger.error(error);
});
Runningourexamplegivesus:
[~/examples/example-13]$nodeindex.js|bunyan
[2015-06-07T13:03:57.047Z]ERROR:example-13/4598onmacbook.local:Hello
errors
Error:Helloerrors
at/Users/fabian/examples/example-13/index.js:12:9
at_fulfilled(/Users/fabian/examples/example-
13/node_modules/q/q.js:834:54)
Thisiswhereitstartstogetdifficulttoread;assigningsimplenamestoourfunctionscanhelpusfindwheretheerroriscomingfrom:
returnQ()
.then(functionresultFromOtherFunction(){
thrownewError('Helloerrors');
});
Runningtheexample:
[~/examples/example-13]$nodeindex.js|bunyan
[2015-06-07T13:04:45.598Z]ERROR:example-13/4614onmacbook.local:Hello
errors
Error:Helloerrors
atresultFromOtherFunction(/Users/fabian/examples/example-
13/index.js:12:9)
at_fulfilled(/Users/fabian/examples/example-
13/node_modules/q/q.js:834:54)
ErrorhandlingAnotheraspectofdebuggingishandlingandexpectingerrorsbeforehand.Therearethreewaysinwhichwecanhandleourerrors:
asimpletry/catchcatchingthemattheprocesslevelcatchingerrorsonthedomainlevel
Atry/catchfunctionwillbesufficientifweexpectanerrortooccurandwewillbeabletocontinuewithoutknowingtheresultofwhateverwasbeingexecuted,orwecouldhandleandreturntheerror,asshown:
functionparseJSONAndUse(input){
varjson=null;
try{
json=JSON.parse(input);
}catch(error){
returnQ.reject(newError("Couldn'tparseJSON"));
}
returnQ(use(json));
}
Anothersimplewaytocatcherrorsistoaddanerrorhandlertoyourprocess;anyerrorsthatarecaughtatthislevelareusuallyfatalandshouldbetreatedassuch.Anexitoftheprocessshouldfollowandyoushouldbeusingapackage,suchasforeverorpm2:
process.on('uncaughtException',functionerrorProcessHandler(error){
logger.fatal(error);
logger.fatal('Fatalerrorencountered,exitingnow');
process.exit(1);
});
Youshouldalwaysexittheprocessfollowinganuncaughterror.Thefactthatitisuncaughtmeansthatyourapplicationisinanunknownstatewhereanythingcanhappen.Forexample,therecouldhavebeenanerrorinyourHTTProuterandnomorerequestscanberoutedtothecorrecthandlers.Youcanreadmoreaboutthisathttps://nodejs.org/api/process.html#process_event_uncaughtexception.
Abetterwaytohandleerrorsonagloballevelisusingdomain.Withdomainsyoucanalmostsandboxagroupofasynchronouscodetogether.
Let’sthinkinthecontextofarequesttoourserver.Wemakearequest,readfromadatabase,makecallstoexternalservices,writebacktoadatabase,dosomelogging,dosomebusinesslogic,andweexpectperfectdatacomingfromexternalsourcesallaroundthecode.However,intherealworlditisn’talwayssoandwecan’thandleeveryerrorthatcouldpossiblyoccur;moreover,wedon’twanttotakedownourentireserverjustbecauseofoneerrorforaveryspecificrequest.That’swhereweneeddomains.
Let’slookatthefollowingexample:
varDomain=require('domain'),
domain;
domain=Domain.create();
domain.on('error',function(error){
console.log('Domainerror',error.message);
});
domain.run(function(){
//Runcodeinsidedomain
console.log(process.domain===domain);
thrownewError('Errorhappened');
});
Let’srunthecode:
[~/examples/example-14]$nodeindex.js
true
DomainerrorErrorhappened
Thereisaproblemwiththiscode;however,aswearerunningthissynchronouslywearestillputtingtheprocessintoabrokenstate.Thisisbecausetheerrorbubbleduptothenodeitselfandthenwaspassedtotheactivedomain.
Whenwearecreatingthedomaininanasynchronouscallback,wecanbesurethattheprocesscancontinue.Wecanmimicthisbyusingprocess.nextTick:
process.nextTick(function(){
domain.run(function(){
thrownewError('Errorhappened');
});
console.log("Iwon'texecute");
});
process.nextTick(function(){
console.log('Nexttickhappend!');
});
console.log('Ihappenedbeforeeverythingelse');
Runningtheexampleshoulddisplaythecorrectlogs:
[~/examples/example-15]$nodeindex.js
Ihappenedbeforeeverythingelse
DomainerrorErrorhappened
Nexttickhappend!
SummaryInthischapterwehavecoveredafewpost-mortemdebuggingmethodstohelpusuncoverbugsincludinglogging,namingpractices,andsufficienterrorhandling.
Inthenextchapter,wewillcoverconfigurationofourapplications.
Chapter5.ConfigurationAsourapplicationsgetlargerandlarger,westarttolosesightofwhatisconfiguredtodowhat;wemayalsogetintoasituationwherewehavecoderunningin12differentplaces,eachneedingabitofcodethathastobechangedtodosomethingelse,forexampleconnectingtoadifferentdatabase.Then,foreachofthose12environments,wehavethreeversions:production,staging,anddevelopment.Allofasudden,itgetsverycomplicated.Thisiswhyweneedtobeabletoconfigureourcodefromahigher-levelsothatwedon’tbreakanythingintheprocess.
JSONfilesThereareafewwaysinwhichwecanconfigureourapplication.ThefirstwaythatwewilllookatisasimpleJSONfile.
Ifwelookattheextensionsrequiresupportsbydefault,wecanseethatwecanimportJSONrightintoourcode,asshown:
[~/examples/example-16]$node
>require.extensions
{'.js':[Function],
'.json':[Function],
'.node':[Function:dlopen]}
Let’screateasimpleserverwithaconfigurationfileratherthanahardcodedfile:
First,wehavetocreatetheconfigurationfile:
{
"host":"localhost",
"port":8000
}
Withthis,wecannowcreateourserver:
varConfig=require('./config.json'),
Http=require('http');
Http.createServer(function(request,response){
}).listen(Config.port,Config.host,function(){
console.log('Listeningonport',Config.port,'andhost',Config.host);
});
Now,wecanjustchangetheconfigfileinsteadofchangingthecodetochangetheportonwhichourserverisrunning.
Butourconfigfileisabittoogeneric;wehavenoideaastowhatisahostoraportandwhattheyarerelatedto.
Whileconfiguring,thekeysneedtobelessgenericsothatweknowwhattheyarebeingusedfor,unlessthecontextisgivendirectlybytheapplication.Forexample,iftheapplicationwastoservepurelystaticcontentthenitmaybeacceptabletohavemoregenerickeys.
Tomaketheseconfigurationkeyslessgeneric,wecanwrapthemallinaserverobject:
{
"server":{
"host":"localhost",
"port":8000
}
}
Sonow,inordertoknowabouttheportoftheserverweneedtousethefollowingcode:
Config.server.port
Anexamplewherethiswillbeusefulcouldbeforaserverthatconnectstoadatabase,astheycanacceptboththeportandhostastheparameters:
{
"server":{
"host":"localhost",
"port":8000
},
"database":{
"host":"db1.example.com",
"port":27017
}
}
EnvironmentalvariablesAnotherwayinwhichwecanconfigureourapplicationsisthroughtheuseofenvironmentalvariables.
Thesecanbedefinedbytheenvironmentyouarerunningyourapplicationinorinthecommandthatyouareusingtostartyourprocesswith.
InNode.js,youcanaccesstheenvironmentalvariablesusingprocess.env.Whenusingenv,youdon’twanttobepollutingthisspacetoomuchandsoitisagoodideatoprefixthekeywithsomethingrelatedtoyourself—yourprogramorcompany.Forexample,Config.server.hostbecomesprocess.env.NAME_SERVER_HOST;thereasonforthisisthatwecanclearlyseewhatisrelatedtoyourprogramandwhatisn’t.
Usingenvironmentalvariablestoconfigureourserver,ourcodewilllookasfollows:
varHttp=require('http'),
server_port,
server_host;
server_port=parseInt(process.env.FOO_SERVER_PORT,10);
server_host=process.env.FOO_SERVER_HOST;
Http.createServer(function(request,response){
}).listen(server_port,server_host,function(){
console.log('Listeningonport',server_port,'andhost',server_host);
});
Torunthiscodewithourvariables,wewilluse:
[~/examples/example-17]$FOO_SERVER_PORT=8001\
FOO_SERVER_HOST=localhostnodeserver.js
Listeningonport8001andhostlocalhost
YouprobablynoticedthatIhadtouseparseIntforFOO_SERVER_PORT;thisisbecauseallvariablespassedinthismannerareessentiallystrings.Wecanseethisbyexecutingtypeofprocess.env.FOO_ENV:
[~/examples/example-17]$FOO_ENV=1234node
>typeofprocess.env.FOO_ENV
'string'
>typeofparseInt(process.env.FOO_ENV,10)
'number'
Althoughthiskindofconfigurationisverysimpletocreateandconsume,itmaynotbethebestmethod,asthevariablesarehardtokeeptrackofiftherearealotofthemandtheycanbedroppedveryeasily.
ArgumentsAnotherwayinwhichtheconfigurationcanbedoneisthroughtheuseofargumentsthatarepassedtoNode.jsastheprocessstarts,youcanaccesstheseusingprocess.argv,withargvstandingforargumentvector.
Thearraythatprocess.argvreturnswillalwayshaveanodeatindex0.Forexample,ifyourunnodeserver.jsthenprocess.argvwillhavethevalueof['node','/example/server.js'].
IfyoupassanargumenttoNode.jsthenitwillbeaddedtotheendofprocess.argv.
Ifyourunnodeserver.js--port=8001,theprocess.argvwillcontain['node','/example/server.js','--port=8001'],prettysimple,right?
Eventhoughwecanhaveallthisconfiguration,weshouldalwaysrememberthatconfigurationcanbesimplyexcludedandwewillstillwantourapplicationtorunwhenthishappens.Usually,youshouldprovidedefaulthardcodedvaluesasabackupwhenyouhaveconfigurationoptions.
Parameterssuchaspasswordsandprivatekeysshouldneverhaveadefaultvaluebutlinksandoptionsthatareusuallystandardshouldbegivendefaults.ItisprettyeasytogiveadefaultvalueinNode.js,allyouneedtodoisusetheORoperator.
value=value||'default';
Essentially,whatthisdoesischeckifthevalueisfalsy;ifitis,thenusethedefaultvalue.Youneedtowatchoutforvaluesthatyouknowcouldbefalsy,booleansandnumbersdefinitelyfallintothiscategory.
Inthesecasesyoucanuseanifstatementcheckingforanullvalue,asshown:
if(value==null)value=1
SummaryThat’sallforconfiguration.Inthischapteryoulearnedaboutthethreemethodsthatyoucanusetocreateadynamicapplication.Welearnedthatweshouldnameourconfigurationkeysinawaythatwecanidentifywhatthevaluesarechangingtoandhowtheywillaffectourapplication.Wealsolearnedabouthowwecanpasssimpleargumentstoourapplicationusingenvironmentalvariablesandargv.
Withthisinformation,wecanmoveforwardtoconnectingandutilizingdatabasesinthenextchapter.
Chapter6.LevelDBandNoSQLInthischapter,wewillcovertwovariationsofdatabasesthatcanbeusedwithNode.js;oneprovidesaverylightweightandsimplesetoffeatures,whiletheothergivesusmoreflexibilityandageneral-purposesetoffeatures.Inthischapter,wearegoingtocoverLevelDBandMongoDB
LevelDBOneofthegreatthingswithNode.jsisthatweusethesamelanguageforboththefrontandbackendandthesamegoesforNoSQLdatabases.ThemajorityofthemsupportJSONrightoffthemark;thisisgreatforanyoneusingNode.jsasthereisnotimespentinmakingarelationalmodel,turningitintoaJSON-likestructure,passingittothebrowser,doingsomethingwithit,andreversingtheprocess.
WithadatabasethatsupportsJSONnatively,youcangetrightdowntobusinessandplayball.
GooglehasprovideduswithasimplehookintoaNoSQLdatabasethatcanbeinstalledandcanbemadereadytousewithjustonecommand:
[~/examples/example-18]$npminstalllevel
YouwillseethatthiswillinstallbothLevelDOWNandLevelUP.
LevelDOWNisthelow-levelbindingtoLevelDBandLevelUPisthesimplewrapperaroundthis.
LevelDBisverysimpleintermsofsetup.Onceitisinstalled,wejustcreateaninstanceofLevelUPandpassitwherewewantourdatabasetobestored:
varLevelUP=require('level'),
db=newLevelUP('./example-db');
Nowwehaveafastandsimplewaytostoredata.
AsLevelDBisjustasimplekey/valuestore,itdefaultstostringkeysandstringvalues.Thisisusefulifthat’salltheinformationyouwishtostore.Youcanalsouseitasasimplecachestore.IthasaverysimpleAPI,atthisstageweareonlygoingtofocusonfourmethods:put,get,del,andcreateReadStream;it’sprettyobviouswhatmostofthemdo:
Method Usedfor Arguments
put insertingpairs key,value,callback(error)
get fetchingpairs key,callback(error,value)
del deletingpairs key,callback(error)
createReadStream fetchingmanypairs
Toinsertdataoncewehavecreatedourdatabase,allweneedtodois:
db.put('key','value',function(error){
if(error)returnconsole.log('Error!',error)
db.get('key',function(error,value){
if(error)returnconsole.log('Error!',error)
console.log("key=",value)
});
});
Ifwerunthecode,wewillseethatweinsertedandretrievedourvalue:
[~/examples/example-18]$nodeindex.js
key=value
Thisisn’toursimpleJSONstructure;however,it’sjustastring.TogetourstoretosaveJSON,allweneedtodoistopassthevalueencodingasanoptiontothedatabase,asshown:
varLevelUP=require('level'),
db=newLevelUP('./example-db',{
valueEncoding:'json'
});
NowwecanstoreJSONdata:
db.put('jsonKey',{inner:'value'},function(error){
if(error)returnconsole.log('Error!',error)
db.get('jsonKey',function(error,value){
if(error)returnconsole.log('Error!',error)
console.log("jsonKey=",value)
});
});
However,astringcanbestoredasJSONandwecanstillpassstringsasavalueandalsoretrieveitassuch.
Runningthisexamplewillshowthefollowing:
[~/examples/example-18]$nodeindex.js
key=value
jsonKey={inner:'value'}
Now,wehavethesimplemethodsdownandwecannowmoveontocreateReadStream.
ThisfunctionreturnsanobjectthatcanbecomparedtoNode.jsbuiltinReadableStream.Foreachkey/valuepairinourdatabase,itwillemitadataevent;italsoemitsotherevents,suchaserrorandend.Iferrordoesn’thaveaneventlistener,thenitwillpropagate,therebykillingyourentireprocess(ordomain),asshown:
db.put('key1',{inner:'value'},function(error){
if(error)returnconsole.log('Error!',error)
varstream=db.createReadStream();
stream
.on('data',function(pair){
console.log(pair.key,"=",pair.value);
})
.on('error',function(error){
console.log(error);
})
.on('end',function(){
console.log('end');
});
});
Runningthisexample:
[~/examples/example-20]$nodeindex.js
key1={inner:'value'}
end
Ifweputmoredatainthedatabasewewillhavemultipledataeventsemitted:
[~/examples/example-20]$nodeindex.js
key1={inner:'value'}
key2={inner:'value'}
end
MongoDBAsyoucansee,databaseswithNode.jscanbeverysimple.IfwewantsomethingabitmorecompletewecanuseanotherNoSQLdatabasecalledMongoDB–anotherverypopulardocument-baseddatabase.
Forthissetofexamples,youcaneitheruseahosteddatabaseusingaprovidersuchasMongoLab(theyprovideafreetierfordevelopment)oryoucansetupadatabaselocallyfollowingtheinstructionsathttp://docs.mongodb.org/manual/installation.
Wecancontinueonceyouhaveadatabasetoconnectto.
MongoDBhasseveralmodulesthatcanbeusedwithNode.js,themostpopularoneisMongoose;however,wewillbeusingthecoreMongoDBmodule:
[~/examples/example-21]$npminstallmongodb
Touseourdatabase,wefirstneedtoconnecttoit.Weneedtoprovidetheclientwithaconnectionstring,agenericURIwiththeprotocolofmongodb.
Ifyouhavealocalmongodatabaserunningwithnocredentialsyouwilluse:
mongodb://localhost:27017/database
Thedefaultportis27017,soyoudon’tneedtospecifythat;however,itisincludedforcompleteness.
IfyouareusingMongoLab,theywillprovideyouwithaconnectionstring;itshouldbeintheformatof:
mongodb://<dbuser>:<dbpassword>@<ds>.mongolab.com:<port>/<db>
Connectingtoourdatabaseisactuallyprettysimple.Allweneedtodoisprovidethedriverwithaconnectionstringandwegetbackadatabase:
varMongoDB=require('mongodb'),
MongoClient=MongoDB.MongoClient;
connection="mongodb://localhost:27017/database"
MongoClient.connect(connection,function(error,db){
if(error)returnconsole.log(error);
console.log('Wehaveaconnection!');
});
EachsetofdatainMongoDBisstoredinacollection.Oncewehaveadatabasewecanfetchacollectiontoruntheoperationson:
varcollection=db.collection('collection_name');
Inacollection,wehaveafewsimplemethodsthatholdlotsofpower,givingusafullCRUD“API”.
EachdocumentinMongoDBhasanid,whichisaninstanceofObjectId.Theproperty
theyuseforthisidis_id.
Tosaveadocumentwejustneedtocallsave,itacceptsanobjectoranarrayofobjects.Asingleobjectinacollectionisreferredtoasadocument:
vardoc={
key:'value_1'
};
collection.save(doc,{w:1},function(){
console.log('Documentsaved')
});
IfwecallthesavefunctionwithadocumentthathasanIDthenthedocumentwillbeupdatedratherthaninserted:
varObjectId=MongoDB.ObjectId
//Thisdocumentalreadyexistsinmydatabase
vardoc_id={
_id:newObjectId("55b4b1ffa31f48c6fa33a62a"),
key:'value_2'
};
collection.save(doc_id,{w:1},function(){
console.log('DocumentwithIDsaved');
});
Nowthatwehavedocumentsinourdatabase,wecanqueryforthem,asshown:
collection.find().toArray(function(error,result){
console.log(result.length+"documentsinourdatabase!")
});
Ifnocallbackisprovidedtofindthenitwillreturnacursor;thisallowsustousemethodssuchaslimit,sort,andtoArray.
Youcanpassaquerytofindtolimitwhatisreturned.InordertofindanobjectbyitsIDweneedtousesomething,suchas:
collection.find(
{_id:newObjectId("55b4b1ffa31f48c6fa33a62a")},
function(error,documents){
console.log('Founddocument',documents[0]);
}
);
Wecanalsofilteritbyanyotherpropertyyoumightuse:
collection.find(
{key:'value'},
function(error,documents){
console.log('Found',documents.length,'documents');
}
);
IfyouhaveusedSQLbefore,youmusthavenoticedthelackofoperators,suchasOR,AND,orNOT.However,youdon’tneedtoworrybecausemongoprovidesmanyequivalents.
Youcanseeacompletelisthere:
http://docs.mongodb.org/manual/reference/operator/query/.
Alloperatorsareprefixedwiththedollarsign,forexample$and,$or,$gt,and$lt.
Youcanseethespecificsyntaxtousethesebylookingatthedocumentation.
Tousean$orcondition,youneedtoincludeitasifitisaproperty:
collection.find(
{
$or:[
{key:'value'},
{key:'value_2'}
]
},
function(error,documents){
console.log('Found',documents.length,'documents');
}
);
UsingadatabasesuchasMongoDBgivesusmorepowertoretrieveourdataandcreateamorefeaturefullsoftware.
SummaryNowwehaveplaceswherewecanstoreourdata.Ononeendwehaveasimplekey/valuestorethatprovidesuswithasuper-convenientwaytostoredataandontheotherendwehaveafeaturefulldatabasethatprovidesuswithafullsetofqueryoperators.
Boththesedatabaseswillhelpusinthenextchaptersaswemoveclosertocreatingourfullstackapplication.
InthenextchapterwewillcoverSocket.IO,areal-timecommunicationframeworkbuiltontopofWebSockets.
Chapter7.Socket.IOSimpleHTTPisgreatforthingsthatdon’tneedreal-timedata,butwhataboutwhenweneedtoknowaboutthingsastheyhappen.Forexample,ifwewerecreatingawebsitethathadachatinterfaceorsimilar?
ThisiswhensomethinglikeWebsocketscomeintoplay.WebsocketsareusuallyreferredtoasWebSocketsandarefullduplexortwo-waylow-latencycommunicationchannels.Theyaregenerallyusedbymessagingapplicationsandgameswheremessagesneedtoberelayedbetweentheserverandclient.Thereisareallyhandynpmmodulecalledsocket.io,whichcanaddWebsocketstoanyNode.jsapplication.
Toinstallitwejustneedtorun:
[~/examples/example-27]npminstallsocket.io
Socket.IOcanbesetupverysimplytolistenforconnections.First,wewanttobeabletoserveoutastatichtmlpagetorunclientsidecodewith:
varHttp=require('http'),
FS=require('fs');
varserver=Http.createServer(handler);
server.listen(8080);
functionhandler(request,response){
varindex=FS.readFileSync('index.html');
index=index.toString();
response.writeHead(200,{
'Content-Type':'text/html',
'Content-Length':Buffer.byteLength(index)
});
response.end(index);
}
Now,letscreateanHTMLfileaswell,namedindex.html,inthesamedirectory:
<html>
<head>
<title>WSExample</title>
</head>
<body>
<h2>WSExample</h2>
<pid="output"></p>
<!--SocketIOClientlibrary-->
<scriptsrc="/socket.io/socket.io.js"></script>
<scripttype="application/javascript">
/*Ourclientsidecodewillgohere*/
</script>
</body>
</html>
Let’srunourexampleandensurethatwegetourpage,weshouldbeabletoseeWSExampleonscreen.Now,toaddsocketsupporttoourapplicationwejustneedtorequiresocket.ioandspecifywhathttpservertolistenwithtoIOServer:
varIOServer=require('socket.io');
vario=newIOServer(server);
Now,wheneverthereisanewsocketconnectionover8080wewillgetaconnectioneventonio:
io.on('connection',function(socket){
console.log('NewConnection');
});
Letsaddsomecodetotheclient.Socket.IOprovidesuswithaclientlibraryandtheyexposethisthroughtheendpoint/socket.io/socket.io.js.Thisisincludedintheprecedingindex.htmlfile.
TipAlltheclientsidecodeiscontainedwithinthesecondscripttagoftheindex.htmlfile.
Tocreateaconnectionwiththeserverallweneedtodoiscallio.connectandpassthelocation.Thiswillreturnasocketforuswithwhichwecancommunicatetoourserver.
WeareusingtheclientprovidedbySocket.IOhere,asitwilldetectwhetherWebSocketsareavailable,andifpossibleusethem.Otherwise,itwillutilizeothermethodssuchaspolling,whichmakessurethatitworkseverywhereratherthanjustonevergreenbrowsers:
varsocket=io.connect('http://localhost:8080');
Wewilluseapelementtologmessagestothescreenwith.Wecandothatwiththiscode,thenallweneedtodoiscalllogScreen:
varoutput=document.getElementById('output');
functionlogScreen(text){
vardate=newDate().toISOString();
line=date+""+text+"<br/>";
output.innerHTML=line+output.innerHTML
}
Onceaconnectionismade,justlikeontheserversideaconnectioneventisemitted,wecanlistentothisusingon:
socket.on('connection',function(){
logScreen('Connection!');
});
Now,wecanrunourserveroncewenavigatetohttp://localhost:8080.YoushouldbeabletoseeConnection!showingup:
Toreceiveamessageonserverside,wejustneedtolistenforthemessageevent.Fornow,wewilljustechothemessageback:
socket.on('connection',function(){
socket.on('message',function(message){
socket.send(message);
});
});
Ontheclientside,wejustneedtocallsendtosendamessageandwewanttodothisinsideourconnectionevent.Theapioneachsideisverysimilartoeachother,asyoucansee:
socket.send('Hello');
Ontheclientside,wealsowanttolistenformessagesandlogthemtothescreen:
socket.on('message',logScreen);
Oncewerestarttheserverandrefreshourpage,weshouldbeabletoseeanadditionalHellomessageappearonscreen.
[~/examples/example-27]$nodeindex.js
Hello
Thishappensbecausetheservercannowsendtheclientpacketsofdata.Italsomeansthatwecanupdatetheclientatanytime.Forexample,everysecondwecansendanupdatetotheclient:
socket.on('connection',function(){
functiononTimeout(){
socket.send('Update');
}
setInterval(onTimeout,1000);
});
Now,whenwerestartourserverweshouldbeabletoseeanupdatemessageeverysecond.
Youmighthavenoticedthatyoudidn’tneedtorefreshyourwebpagefortheconnectiontobeopenedagain.Thisisbecausesocket.iotransparentlykeepsourconnections“alive”
aswellasreconnectingifneeded.Thistakesallthepainoutofusingsockets,aswehavenoneofthesetroubles.
RoomsSocket.IOalsohastheconceptofrooms,wheremultipleclientscanbegroupedintodifferentrooms.Toemulatethis,allyouwillneedtodoisnavigatetohttp://localhost:8080inmultipletabs.
Onceaclientconnects,weneedtocallthejoinmethodtotellthesocketwhatroomtobein.Ifwewishtodosomethingsuchasagroupchatwithspecificusersonly,weneedhavearoomidentifierinadatabaseorcreateone.Fornowwewilljusthaveeveryonejointhesameroom:
socket.on('connection',function(){
console.log('NewConnection');
varroom='ourroom';
socket.join(room,function(error){
if(error)returnconsole.log(error);
console.log('Joinedroom!');
});
});
Foreverytabweopen,weshouldseeamessagethatwehavejoinedaroom:
[~/examples/example-27]$nodeindex.js
NewConnection
Joinedroom!
NewConnection
Joinedroom!
NewConnection
Joinedroom
Withthis,wecanbroadcastamessagetotheentireroom.Let’sdothiseverytimesomeonejoins.Withinthejoincallback:
socket
.to(room)
.emit(
'message',
socket.id+'joinedtheroom!'
);
Ifyoulookinyourbrowser,witheachconnectiontheotherclientsgetanotificationthatsomeoneelsehasjoined:
x3OwYOkOCSsa6Qt5AAAFjoinedtheroom!
mlx-Cy1k3szq8W8tAAAEjoinedtheroom!
Connection!
Connecting
Thisisgreat,wecannowcommunicatealmostdirectlybetweenbrowsers!
Ifwewanttoleavearoom,allweneedtodoiscallleave,wewillbroadcastthisbeforewecallthefunction:
socket
.to(room)
.emit(
'message',
socket.id+'isleavingtheroom'
);
socket.leave(room);
Whilerunningthis,youwillnotseeanymessagesfromanotherclientbecauseyouareleavingrightaway:however,ifyouweretoputadelayonthisyoumightseeanotherclientcomeandgo:
leave=function(){
socket
.to(room)
.emit(
'message',
socket.id+'isleavingtheroom'
);
socket.leave(room);
};
setTimeout(leave,2000);
AuthenticationForauthentication,wecanusethesamemethodthatweusedwithourHTTPserverandwecanacceptaJSONWebToken
Intheseexamples,forsimplicitywewilljusthaveasingleHTTProutetologin.WewillsignaJWTthatwewilllaterauthenticatebycheckingthesignature
Weneedtoinstallacoupleofextranpmmodulesforthis;wewillincludechancesothatwecangeneratesomerandomdata.
[~/examples/example-27]npminstallsocketio-jwtjsonwebtokenchance
First,wearegoingtoneedaroutetologin.Wewillmodifyourhandlertowatchfortheurl/login:
if(request.url==='/login'){
returngenerateToken(response)
}
OurnewfunctiongenerateTokenwillcreateaJSONWebTokenwithsomerandomdatausingchance.Wewillalsoneedasecretforourtokens:
varJWT=require('jsonwebtoken'),
Chance=require('chance').Chance();
varjwtSecret='Oursecret';
functiongenerateToken(response){
varpayload={
email:Chance.email(),
name:Chance.first()+''+Chance.last()
}
vartoken=JWT.sign(payload,jwtSecret);
response.writeHead(200,{
'Content-Type':'text/plain',
'Content-Length':Buffer.byteLength(token)
})
response.end(token);
}
Now,wheneverwerequesthttp://localhost:8080/loginwewillreceiveatokenthatwecanuse:
[~]$curl-XGEThttp://localhost:8080/login
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbW
joiR2VuZSBGbGVtaW5nIiwiaWF0IjoxNDQxMjcyMjM0
e1Y
Wecanenterthisintothedebuggerathttp://jwt.io/andseethecontents:
{
"email":"jefoconeh@ewojid.io",
"name":"GeneFleming",
"iat":1441272234
}
Awesome,wehaveatokenandarandomuserbeinggeneratedforus.Now,wecanusethistoauthenticateourusers.Socket.IOhasamethodontheservertodothisandwejustneedtopassahandlertypefunctiontoit.Thisiswheresocketio-jwtcomesin,wepassitoursecretanditwillensureitisarealtoken,prettysimple:
varSocketIOJWT=require('socketio-jwt');
io.use(SocketIOJWT.authorize({
secret:jwtSecret,
handshake:true}));
Now,whenwetrytoconnecttoourserverfromtheclientitwillneveremittheconnectevent,asourclientisn’tauthenticated.Thisisexactlywhatwewant.
WefirstwanttowrapupourSocket.IOcode(wewillcallthislater);wealsowanttogiveitaparameteroftoken:
functionsocketIO(token){
varsocket=io.connect('http://localhost:8080');
varoutput=document.getElementById('output');
functionlogScreen(text){
vardate=newDate().toISOString();
line=date+""+text+"<br/>";
output.innerHTML=line+output.innerHTML
}
logScreen('Connecting');
socket.on('connect',function(){
logScreen('Connection!');
socket.send('Hello');
});
socket.on('message',logScreen);
}
Next,wewillcreatealoginfunction,thiswillrequesttheloginURLandthenpasstheresponsetothesocketIOfunction,asshown:
functionlogin(){
{
varrequest=newXMLHttpRequest();
request.onreadystatechange=function(){
if(
request.readyState!==4||
request.status!==200
)return
socketIO(request.responseText);
}
request.open("GET","/login",true);
request.send(null);
}
Thenwewanttocalltheloginfunction:
login();
Wecanpassthetokenontotheserverbychangingtheconnectcalltopassaquerystring:
varsocket=io.connect('http://localhost:8080',{
query:'token='+token
});
Now,whenrunningourserverandnavigatingtoourclientweshouldbeabletoconnect—awesome!Sincewehaveauthenticatedwecanalsorespondwithapersonalizedmessageforeachuser,insideourserver-sideconnectioneventhandlerwewillemitamessagetotheclient.
Oursocketwillhaveanewpropertycalleddecoded_token;usingthiswewillbeabletoviewthecontentsofourtoken:
varpayload=socket.decoded_token;
varname=payload.name;
socket.emit('message','Hello'+name+'!');
Oncewejoinourroom,wecantelltherestoftheclientswhohavealsojoined:
socket
.to(room)
.emit(
'message',
name+'joinedtheroom!'
);
SummarySocket.IObringsamazingcapabilitiestoourapplications.Wecannowinstantlycommunicatewithothers,eitherindividuallyorbybroadcastinginaroom.Withtheabilitytoidentifyusers,wecanrecordmessagesorthehistoryofthatuser,readytobeservedupbyaRESTfulAPI.
Wearenowreadytobuildreal-timeapplications!
Chapter8.CreatingandDeployingPackagesNowthatwehaveallofthepiecesthatareneededtocreateNode.jsapplicationsandservers,wewillnowfocusmoreonsharingourmodulesandcontributingtotheeco-system.
Allthepackagesonnpmhavebeenuploaded,maintained,andcontributedbysomeoneinthecommunity,solet’shavealookathowwecandothesameourselves.
CreatingnpmpackagesWecanstartwiththefollowingsteps:
Firstweneedtocreateauser:
[~]$npmadduser
Username:<username>
Password:
Email:(thisISpublic)<email>
Oncewehaveauserwehaveopenedthegatestonpm.
Now,let’screateapackage:
[~/examples/example-22]$npminit
{
"name":"njs-e-example-package",
"version":"1.0.0",
"description":"",
"main":"index.js",
"scripts":{
"test":"echo\"Error:notestspecified\"&&exit1"
},
"author":"",
"license":"ISC"
}
Topublishthispackageallweneedtodoisrunnpmpublish:
[~/examples/example-22]$npmpublish
+njs-e-example-package@1.0.0
Youcanseethatwehavepublishedourpackagesuccessfully,youcanviewtheoneIpublishedat:
https://www.npmjs.com/package/njs-e-example-package
Youwillhavetonameyourpackagesomethingelseinordertopublishit;otherwise,wewillhaveaconflict.
Nowwecanrunthefollowingcommand:
[~/examples/example-21]$npminstallnjs-e-example-package
njs-e-example-package@1.0.0node_modules/njs-e-example-package
Thenwewillhavethepackage!Isn’tthatprettycool?
Ifwetrytopublishagain,wewillgetanerrorbecauseversion1.0.2isalreadypublished,asshowninthefollowingscreenshot:
Toincrementourpackageversion,allweneedtodoisexecute:
[~/examples/example-22]$npmversionpatch
v1.0.1
Nowwecanpublishagain:
[~/examples/example-22]$npmpublish
+njs-e-example-package@1.0.1
Youcangotoyourpackagespageonnpmandyouwillseethattheversionnumberandreleasecounthasbeenupdated.
VersioninginNode.jsfollowsthesemverschema,whichismadeupofmajor,minor,andpatchversions.Whenthepatchversionisincremented,itmeansthattheAPIhasstayedthesamehoweversomethinghasbeenfixedbehindthescenes.Iftheminorversionhasbeenincremented,itmeansthatanon-breakingAPIchangehasoccurred,suchasamethodhasbeenadded.Ifthemajorversionisupdated,itmeansthattherehasbeenabreakingAPIchange;forexampleamethodhasbeendeletedoramethodsignaturehaschanged.
Sometimes,therearethingsinyourprojectthatyoudon’twanttobepushedoutforotherpeopletohave.Thiscouldbeanoriginalsource,somecertificates,ormaybesomekeysfordevelopment.Justlikewhenusinggit,wehaveanignorefilecalled.npmignore.
Bydefault,ifthereisno.npmignorebutthereisa.gitignore,npmwillignorewhatismatchedbythe.gitignorefile.Ifyoudon’tlikethisbehaviorthenyoucanjustcreateanempty.npmignorefile.
The.npmignorefilefollowsthesamerulesas.gitignore,whichareasfollows:
Blanklinesorlinesstartingwith#areignoredStandardglobpatternsworkYoucanendpatternswithaforwardslash/tospecifyadirectoryYoucannegateapatternbystartingitwithanexclamationpoint!
Forexample,ifwehadadirectoryofcertificatesinwhichwehadakey:
[~/examples/example-22]$mkdircertificates
[~/examples/example-22]$touchcertifticates/key.key
Weprobablydon’twantthistobepublished,soinourignorefilewewillhave:
certificates/
Wealsodon’twantanykeyfilesthatwehavelyingaround,soweaddthisaswell:
*.key
Now,let’spublish:
[~/examples/example-22]$npmversionpatch
v1.0.2
[~/examples/example-22]$npmpublish
+njs-e-example-package@1.0.2
Now,let’sinstallourpackage:
[~/examples/example-23]$npminstallnjs-e-example-package@1.0.2
Now,whenwelistwhat’sinthedirectory,wedon’tseeallourcertificatesbeingpassedaround:
[~/examples/example-23]$lsnode_modules/njs-e-example-package
package.json
Thisisgreat,butwhatifwewanttoprotectourentirepackageandnotjustafewcertificates?
Allweneedtodoissetprivatetotrueinourpackage.jsonfileanditwillpreventnpmfrompublishingthemodulewhenwerunnpmpublish:
Ourpackage.jsonshouldlooksomethinglike:
{
"name":"example-23",
"version":"1.0.0",
"description":"",
"main":"index.js",
"scripts":{
"test":"echo\"Error:notestspecified\"&&exit1"
},
"author":"",
"license":"UNLICENSED",
"dependencies":{
"njs-e-example-package":"^1.0.2"
},
"private":true
}
Now,whenwerunnpmpublish:
[~/examples/example-23]$npmpublish
npmERR!Thispackagehasbeenmarkedasprivate
Awesome,that’sexactlywhatwewantedtosee.
SummaryLookslikewearegettingclosertobeingreadywithallthingsaboutNode.js.Weknownowhowtosetup,debug,develop,anddistributeoursoftware.
Inthenextchapter,wewillcoveronemoreconceptweneedtoknowabout:unittesting.
Chapter9.UnitTestingWehavecomethisfarbuthaven’tdoneanytesting!That’snotverygood,isit?Usually,ifnotalways,testingisamajorconcerninsoftwaredevelopment.Inthischapter,wewillcoverunittestingconceptswithNode.
TherearemanytestingframeworksforNode.jsandinthischapterwewillbecoveringMocha.
InstallingmochaToensurethatmochagetsinstalledeverywhere,weneedtoinstallitglobally.Thiscanbedoneusingthe-gflagwithnpminstall:
[~/examples/example-24]$npminstall-gmocha
Now,wecanuseMochathroughtheterminalconsole.
Typically,wewillcontainallourtestingcodeinatestsub-directorywithintheproject.Allweneedtodotogetourcoderunningisrunmocha,assumingwehavewrittensometestsfirst.
Aswithmany(ifnotall)unittestingframeworks,Mochausesassertionstoensurethatatestrunscorrectly.Ifanerroristhrownandisnothandledthenthetestisconsideredtohavefailed.Whatassertionlibrariesdoisthrowerrorswhenanunexpectedvalueispassed,sothisworkswell.
Node.jsprovidesasimpleassertionmodule,let’shavealookatthefollowing:
[~/examples/example-24]$node
>assert=require('assert')
>expected=1
>actual=1
>assert.equal(actual,expected)
>actual=1
>assert.equal(actual,expected)
AssertionError:2==1
Aswecansee,anerroristhrowniftheassertiondoesn’tpass.However,theerrormessageprovidedwasn’tveryhandy;tofixthiswecanpassanerrormessageaswell:
>assert.equal(actual,expected,'Expected1')
AssertionError:Expected1
Withthiswecancreateatest.
Mochaprovidesmanywaysofcreatingtests,thesearecalledinterfacesandthedefaultiscalledBDD.
Youcanviewallinterfacesathttp://mochajs.org/#interfaces.
TheBDD(BehaviorDrivenDevelopment)interfacecanbecomparedtoGherkinwherewespecifyafeatureandasetofscenarios.Itprovidesmethodstohelpdefinethesesets,describeorcontextisusedtodefineafeature,andtheitorspecifyfunctionsareusedtodefineascenario.
Forexample,ifweweretohaveafunctionthatjoinssomeone’sfirstandlastname,thetestsmightlooksomethinglikethefollowing:
varGetFullName=require('../lib/get-full-name'),
assert=require('assert');
describe('Fetchfullname',function(){
it('shouldreturnbothafirstandlastname',function(){
varresult=GetFullName({first:'Node',last:'JS'})
assert.equal(result,'NodeJS');
})
})
Wecanalsoaddafewmoretestsforthis;forexample,ifitthrewanerrorincaseofnoobjectbeingpassed:
it('shouldthrowanerrorwhenanobjectwasnotpassed',function(){
assert.throws(
function(){
GetFullName(null);
},
/Objectexpected/
)
})
Youcanexploremanymoremocha-specificfeaturesathttp://mochajs.org/.
ChaiAlongwiththemanytestingframeworks,therearealsomanyassertionframeworks,oneofwhichiscalledChai.Completedocumentationcanbefoundathttp://chaijs.com/.
Insteadofjustusingthebuilt-inassertionmoduleprovidedbyNode.js,wemaywanttouseamodulesuchasChaitoextendourpossibilities.
Chaihasthreesetsofinterfaces,should,expect,andassert.Inthischapter,wewillbecoveringexpect.
Whenusingexpect,youareusingnaturallanguagetodescribewhatyouwant;forexample,ifyouwantsomethingtoexist,youcansay,expect(x).to.existratherthanassert(!!x):
varExpect=require('chai').expect
varAssert=require('assert')
varvalue=1
Expect(value).to.exist
assert(!!value)
Usingnaturallanguagemakesthingsalotclearerforpeoplereadingyourtests.
Thislanguagecanbelinkedtogether;wehaveto,be,been,is,that,which,and,has,have,with,at,of,andsame,whichcanhelpustobuildsentenceslike:
Expect(value).to.be.ok.and.to.equal(1)
However,thesewordsareonlyforreliabilityandtheydon’tmodifytheresult.Therearealotofotherwordsthatcanbeusedtoassertthings,suchasnot,exists,ok,andmanymore.Youcanviewthemallathttp://chaijs.com/api/bdd/.
Someexamplesofchaiinuseare:
Expect(true).to.be.ok
Expect(false).to.not.be.ok
Expect(1).to.exists
Expect([]).to.be.empty
Expect('hi').to.equal('hi')
Expect(4).to.be.below(5)
Expect(5).to.be.above(4)
Expect(function(){}).to.be.instanceOf(Function)
StubbingmethodsIfitlookslikeaduck,swimslikeaduck,andquackslikeaduck,thenitprobablyisaduck.
Asyouwriteyourtestsyouwanttobeonlybetestingunitsofcode.Generallythiswillbeamethod,provideitsomeinput,andexpectanoutputofsomekind,orifitisavoidfunction,expectnothingtobereturned.
Withthisinmind,youhavetothinkofyourapplicationasbeinginasandboxedstatewhereitcan’ttalktotheoutsideworld.Forexample,itmightnotbeabletotalktoadatabaseormakeanykindofexternalrequest.Havingthisassumptionisgreatifyouaregoingto(andyouusuallyshould)implementcontinuousintegrationanddeployment.ItalsomeansthattherearenoexternalrequirementsforthemachineyouaretestingonexceptforNode.jsandthetestingframework,whichcouldjustbeapartofyourpackageanyway.
Unlessthemethodyouaretestingisrathersimpleanddoesn’thaveanyexternaldependencies,youwillprobablywanttomockthemethodsthatyouknowitisgoingtoexecute.AgreatmoduletodothisiscalledSinon.js;itallowsyoutocreatestubsandspiestomakesurethatthecorrectdatareturnsfromothermethodsandtoensurethattheywerecalledinthefirstplace.
sinonprovidesmanyhelpers,asmentionedbeforeandoneoftheseiscalledaspy.Aspyisusedmainlytojustwrapafunctiontoseewhatitsinputandoutputwas.Onceaspyhasbeenappliedtoafunction,totheoutsideworlditbehavesexactlythesame.
varSinon=require('sinon');
varreturnOriginal=function(value){
returnvalue;
}
varspy=Sinon.spy(returnOriginal);
result=spy(1);
console.log(result);//Logs1
Wecanuseaspytocheckifafunctionwascalled:
assert(spy.called)
Orwhatargumentswerepassedforeachcall:
assert.equal(spy.args[0][0],1)
Ifweprovidedspywithanobjectandamethodtoreplacethenwecanrestoretheoriginaloncewearefinished.Wewillusuallydothisintheteardownofourtest:
varobject={
spyOnMe:function(value){
returnvalue;
}
}
Sinon.spy(object,'spyOnMe')
varresult=object.spyOnMe(1)
assert(result.called)
assert.equal(result.args[0][0],1)
object.spyOnMe.restore()
Wealsohaveastubfunction,whichinheritsallthefunctionalityofspybutinsteadofcallingtheoriginalfunctionitcompletelyreplacesit.
Thisissowecandefinethebehavior,forexample,whatitreturns:
varstub=Sinon.stub().returns(42)
console.log(stub())//logs42
Wecanalsodefineareturnvalueforasetofargumentspassed:
varstub=Sinon.stub()
stub.withArgs(1,2,3).returns(42)
stub.withArgs(3,4,5).returns(43)
console.log(stub(1,2,3))//logs42
console.log(stub(3,4,5))//logs43
Let’ssaywehadthissetofmethods:
functionUsers(){
}
Users.prototype.getUser=function(id){
returnDatabase.findUser(id);
}
Users.prototype.getNameForUser=function(id){
varuser=this.getUser(id);
returnuser.name;
}
module.exports=Users
Now,weonlycareaboutthescenariowhereauserisreturned,asthegetUserfunctionwillthrowanerrorifitcan’tfindit.Knowingthis,wejustwanttotestthatwhenauserisfounditreturnstheirname.
Thisisaperfectexampleofwhenwewanttostubamethod:
varSinon=require('sinon');
varUsers=require('../lib/users');
varAssert=require('assert');
it('shouldreturnausersname',function(){
varname='NodeJS';
varuser={name:name};
varstub=Sinon.stub().returns(user);
varusers=newUsers();
users.getUser=stub;
varresult=users.getNameForUser(1);
assert.equal(result,name,'Namenotreturned');
});
Insteadofreplacingthefunctionwecanalsopassthefunctionusingthescope,replacingthiswiththeobjectwepassed;eitherwayissufficient.
varresult=users.getNameForUser.call(
{
getUser:stub
},
1
);
SummaryEverythingweneedtocreateaNode.jsapplicationisnowatourfingertips.Testingisjustoneofthosethingsthatareessentialtoanysuccessfulsoftware.Wecoveredusingmochaasatestingframeworkandchaiasanassertionframework.
Inthenextchapter,wewillcoverusinganotherlanguagewithNode.js,CoffeeScript!
Chapter10.UsingMoreThanJavaScriptThroughoutthisbookwehaveusedonlyJavaScript.Well,it’scalledNode.jsisn’tit?
However,thatdoesn’tmeanthatwecan’tuseotherlanguageswithit.WecanandaslongasitcompilestoJavaScriptyouaregoodtogo.
Thereisabiglistofcommonlanguagesthatareavailableat:https://github.com/jashkenas/coffeescript/wiki/list-of-languages-that-compile-to-JS.
Ifyouaremissingyourstronglytypedlanguageorjustwantaslightlydifferentsyntax,thentherewillsurelybeoneoptionoutthereforyousomewhere.
AcoupleofcommonlanguagesincludeCoffeeScriptandTypeScript,theyworkgreatwithNode.jsastheybothcompiletoJavaScript.Inthischapter,wewillcovertheusageofCoffeeScript.TypeScriptissimilarinusage;however,thesyntaxfollowsasimilarpathtoC#andJava.
CoffeeScriptIt’sverysimpletoinstallandstartusingadditionallanguages.Let’shavealookatCoffeeScript:
WeneedtoinstallCoffeeScriptglobally,sothatwecanuseacommandsimilartonode:
[~]npminstall-gcoffee-script
Nowwecanruncoffee:
[~]coffee
>
ThesyntaxisverysimilartoJavaScript:
[~]coffee
>1+1
2
>console.log('Hello')
Hello
Insteadofusingthe.jsextension,weuse.coffee.
First,wewillcreateaCoffeeScriptfile:
/*index.coffee*/
console.log('HelloCoffeeScript!')
Thentorunit,allweneedtodoisusethecoffeecommand,similartothenodecommand:
[~/examples/example-25]coffeeindex.coffee
HelloCoffeScript!
Tocompileour.coffeefilesinto.js,wecanuse-c.Oncecompiled,wecanrunthemdirectlywithNode.js:
[~/examples/example-25]coffee-cindex.coffee
[~/examples/example-25]nodeindex.js
HelloCoffeeScript!
IfwehaveabunchofCoffeeScriptthatwewanttocompiletoJavaScriptallatonce,wecanusecoffee-c-o./lib./src.Thiswilltakeall.coffeefilesfrom./src,compilethemto.jsandthenoutputthemto./lib.
YouwillneedtocompileallyourfilesforotheruserstouseourCoffeeScriptcodealongsidetheirJavaScriptcode.ThealternativeistoincludeCoffeeScriptasadependencyandrequiretheregisterfileintoyourapplication,asshown:
/*index.js*/
require('coffee-script/register');
require('./other.coffee');
YoumayneedtodothisifdonotyouwishtocompileyourCoffeeScript,orifyouareusingatoolthatrequiresaJavaScriptfilesuchasGulporGrunt.
TipToseetheequivalentsbetweenJavaScriptandCoffeeScriptyoucanusethesitehttp://js2.coffee/,itprovidesasimplewaytocomparethetwoonthefly.
CoffeeScriptisbasicallyjustJavaScript;however,ithastargetedreadabilityandsimplicity.WithsimplicityitalsotriestolimittheuseofthebadpartsofJavaScriptandexposesthegoodparts.
UsingCoffeeScriptisusuallygreatforbeginners,(andforexperts),asitusesEnglishlanguageratherthancomputerlanguage.Forexample,insteadofusing===(tripleequals)tocheckiftwovaluesequal,wecanjustusetheEnglishwordis.So,x===ybecomesxisy,whichmeansthatthereisnotranslatingrequiredwhenreading.
Alongwithis,thereareotherkeywords,suchasisnt,not,or,and,yesandno.
Usingthesekeywordsinsteadofsymboloperatorsgivesclaritytothereadersandprogrammers.TheCoffeeScripthassimilarformattingtoPythoninthewayfunctionsandcodeblocksaredeclared;theindentationindicateswhentheblockendsandbegins
CodeblocksandfunctionsInJavaScriptyouwillusuallygrouptogetherblocksusingcurlybraces,asshowninthefollowingexample:
if(true)
{
console.log('Itwastrue!')
}
WhereasinCoffeeScriptyouwillleaveoutallthecurlybraces,infactallthebracesareleftout:
iftrue
console.log('Itwastrue!')
Thesameistruewhendeclaringafunction,noticethatweareusinganarrowratherthanthekeywordfunction.Theparameterlistisonlyrequiredifyouwantnamedarguments:
func=->
console.log('Iexecuted')
CoffeeScripttriestoassumeasmuchaspossiblewhilestillgivingtheprogrammerenoughcontrol.
YoumayhavealsonoticedthatIdidn’tusethevarkeywordwhendeclaringafunction.Thisisbecauseitisimplicitlydeclared,asyoucanseebycompilingtheabovecodetoJavaScript:
varfunc;
func=function()
{
returnconsole.log('Iexecuted');
};
Youcanseeinthiscompiledcodethatthelaststatementinthefunctionisthereturnvalue,thismeansthatwedon’tneedtodeclarethereturnvalueandjustassumethatthelastvalueisreturned.Thismakesitverysimpletocreateonelinefunctions,suchas:
add=(a,b)->a+b
UnlikeJavaScript,youmayprovidedefaultargumentsforafunctionandthiscanbecomparedtoC#;however,it’snotlimitedtoonlyconstantsasitessentiallyexecutesthestatementwithinthefunction:
keys={}
func=(key,date=newDate)->
keys[key]=date
Youcanseethisbycompilingtheabovefunctionas:
varfunc,keys;
keys={};
func=function(key,date)
{
if(date==null)
{
date=newDate();
}
returnkeys[key]=date;
};
Essentially,allCoffeeScriptdoesischeckifthevalueisnullorundefined.
TheexistentialoperatorYoucanchecktoseeifavalueisnullorundefinedusingtheexistentialoperator,whichcheckstoseeifthevalueexists.Thisisindicatedbyusingthequestionmarksymbolafteravariable;thestatementwillbetrueifthevalueexistsandotherwisefalse.
Tousethisinanexpression:
date=null
ifnotdate?
date=newDate()
console.log(date)
Youcanusethisasashorthandoperatoraswell,forexample:
date?=newDate()
console.log(date)
Theabovetwoexamplesofcodewillbehaveexactlythesameandwillactuallycompiletogivethesamecode:
vardate;
date=null;
if(date==null)
{
date=newDate();
}
Youmayalsousetheexistentialoperatortoensureavalueexistsbeforeaccessingapropertyofit.Forexample,ifyouwanttogetthetimefromadate,or-1ifthedatedoesn’texist:
getTime=(date=null)->date?.getTime()?-1
Givingdatethenullvalueshowsthatwedon’tmindifnovalueispassed:
Whenanobjectdoesn’texistandtheoperatorisusedthenthereturnedvalueisundefined,thismeansthatwecanusethesameoperatoragaintoreturnadefaultvalue.
ObjectsandarraysAlongwithalltheassumptionsthatCoffeeScripttriestomake,itsurelydoestrytoremovealltheun-neededsyntaxplainJavaScriptrequires.Anotherinstanceofthiscanbeseenwhiledefiningarraysandobjectsinwhichtheuseofanewlinedeclaresanewitem.Forexample,youwillusuallydefineanarrayas:
array=[
1,
2,
3
]
Thisstillworks;however,withCoffeeScriptyoucanleaveoutthecommasseparatingeachitem:
array=[
1
2
3
]
Youcanalsomixthetwostylestogether:
array=[
'a','b','c'
1,2,3
true,false
]
Youcandothesamewithobjects,suchas:
object={
foo:1
bar:2
}
Withobjectsyoucanevenleaveoutthecurlybracesanduseindentationtoshowthedifferencesintheobject:
object=
foo:1
bar:2
foobar:
another:3
key:4
ToloopanarrayinCoffeeScript,allyouneedtodoisusethefor…inloop,suchas:
forvalue,indexinarray
console.log(value,index)
continueiftypeofvalueis'string'
console.log('Valuewasnotastring')
Ifyoudonotwishtousetheindexofyouritem,yousimplydon’taskforit:
forvalueinarray
console.log(value)
AswithJavaScriptloops,youcanusebreakandcontinuetocontroltheflow.
ToloopanobjectinCoffeeScriptyoucanusethefor…ofloop,thisisabitdifferentfromthefor…ofloopprovidedbyJavaScript:
forkey,valueofobject
console.log(key,value)
Aswiththefor…inloop,ifyoudon’twantthevalue,excludeit:
forkeyofobject
console.log(key)
Forbothtypesofloops,thenamingisirrelevant:
forkey,valueofobject
#Notethatthiswillletdatesandarraysthrough(etc)
continueunlessvalueinstanceofObject
fornestedKey,nestedValueofvalue
console.log(nestedKey,nestedValue)
ClassesUnlikeJavaScript,CoffeeScriptprovidesanaturalwaytodeclareclassesandinheritance.
TodefineaclassinJavaScript,youneedtodeclareafunctionfirst:
functionUser(username){
this.username=username;
}
Thenyouwilldeclaretheprototypemethods:
User.prototype.getUsername=function(){
returnthis.username;
}
Ifyouhaveastaticmethod,youcandefinethisonthefunctionratherthantheprototype:
User.createUser=function(username){
returnnewUser(username);
}
InCoffeeScriptyoucanusetheclasskeywordandgivetheclassaname.Youcanthendeclaretheconstructor,static,andinstance(prototype)methods:
classUser
@createUser:(username)->
returnnewUser(username)
constructor:(username)->
this.username=username
getUsername:->
returnthis.username
Usually,youplaceallyourstaticmethodsaboveyourconstructorsothattheystayseparatefromyourinstancemethods.Thisavoidsconfusion,youmayhavenoticedthatIdeclaredthestaticmethodcreateUserwitha@prefix,thisishowyoudefineastaticmethodinCoffeeScript.However,youcanalsousethetraditionalJavaScriptmethodofUser.createUser=->,eitherwaywillworkhere.
Thecodethatisrunwhentheinstanceisbeingcreatedorconstructediscalledtheconstructor.Thisisthesameterminologythatisusedinmanyotherlanguagessoitshouldbefamiliar.Aconstructorisessentiallyafunction.
Alltheinstancemethodsaredeclaredsimilarlytopropertiesofanobject.
Withclassescomesanothersymbol,the@symbol.Whenusedonaninstance,youcanuseittorefertothethiskeyword.Forexample,thegetUsernamemethodcanbewrittenas:
getUsername:->
return@username
Or,ifwewanttodropthereturnstatementandmakeitaoneliner:
getUsername:->@username
The@symbolcanalsobeusedinparameterliststodeclarethatwewanttheinstancepropertytobesetasthepassedvalue.Forexample,ifwehadasetUsernamemethodwecaneitherdo:
setUsername:(username)->
@username=username
Orwecando:
setUsername:(@username)->
BoththemethodswillcompiletothesameJavaScriptcode.
Giventhefactthatwecanusethe@symbolinourparameterlist,wecanrefactorourconstructorfunctionto:
constructor:(@username)->
AnotheradvantageofusingCoffeeScriptclassisthatwecandefineinheritance.Todoso,allweneedtodoisusetheextendskeyword,thisissimilartootherlanguages.
Intheseexamples,wewanttohavetwoclasses,PersonandRobotthatextendthebaseUserclass.
Forourperson,wewanttobeabletogivethemanameandanagealongwiththeusernamethattheUserclassrequires.
First,weneedtodeclareourclass:
classPersonextendsUser
Thendeclareourconstructor.Inourconstructor,wewillcallthesuperfunction,thiswillexecutetheconstructoroftheparentclassUserandwewanttopasstheusernametoit,asshown:
constructor:(username,@name,@age)->
super(username)
Wethenaddtwomethods,getNameandgetAge:
getName:->@name
getAge:->@age
Next,wewilldothesameforRobot,exceptthistimeweonlywantausernameand@usage:
classRobotextendsUser
constructor:(username,@usage)–>
super(username)
getUsage:->@usage
Wecannowcreateinstancesofourclassesandcomparethem,asshownhere:
SummaryCoffeeScripttriestomakegoodassumptionswithyourcode.ThishelpstoremovesomeproblemsthatJavaScriptdeveloperscomeacross.Forexample,thedifferencebetween==and===.
YoucanlearnmoreaboutthespecificsyntaxofCoffeeScriptathttp://coffeescript.org/.
Inthischapterwehavecoveredutilizinganotherlanguage.ThiscanhelpalleviatethestruggleswithJavaScript’sstyleorsyntaxforbeginners.Forpeoplewhoareusedtomorelanguagefeatures,thisisabigadvantageasithelpsremovethepitfallsthatpeopleusuallycomeacross.
IndexA
argumentsabout/Arguments
authenticationbasicauthentication/Basicauthentication
BBDD(BehaviorDrivenDevelopment)/InstallingmochaBearertoken
about/Bearertokensbunyan
reference/Logging
Cchai
about/ChaiURL/Chai
CoffeeScriptabout/CoffeeScriptinstalling/CoffeeScriptreference/CoffeeScriptcodeblocks/Codeblocksandfunctionsfunctions/Codeblocksandfunctionsexistentialoperator,using/Theexistentialoperatorobjects/Objectsandarraysarrays/Objectsandarraysclasses/Classes
configurationJSONfiles/JSONfilesenvironmentalvariables/Environmentalvariablesarguments/Arguments
Eenvironmentalvariables
about/Environmentalvariableserrorhandling
about/Errorhandlingexistentialoperator
about/Theexistentialoperator
HHellorequireexample
about/HellorequireHTTPmethods
POST/IntroducingroutingGET/IntroducingroutingDELETE/Introducingrouting
JJSONfiles
about/JSONfilesJSONWebToken(JWT)
about/BearertokensURL/Bearertokens
LLevelDB
about/LevelDBlogging
about/Logging
Mmethods
stubbing/Stubbingmethodsmocha
installing/Installingmochareference,forinterfaces/InstallingmochaURL/Installingmocha
MongoDBabout/MongoDB
MongoLababout/MongoDB
morganabout/Loggingreference/Logging
NNode.js
settingup/SettingupURL/Settingup
npmabout/HellonpmURL/Hellonpmscriptsobject/Hellonpm
npmpackagescreating/Creatingnpmpackages
OOAuth
about/OAuthURL/OAuth
Rrouting
about/Introducingrouting
SSinon.js/StubbingmethodsSocket.IO
rooms/Roomsauthentication/Authentication
spy/Stubbingmethodsstubfunction/Stubbingmethods
Ttry/catchfunction/Errorhandling
top related