326

Learning Node.js for .NET Developers€¦ · Isomorphic JavaScript Writing npm packages Defining an npm package Publishing a package to npm Running automated clients on the web

  • Upload
    others

  • View
    56

  • Download
    0

Embed Size (px)

Citation preview

LearningNode.jsfor.NETDevelopers

TableofContents

LearningNode.jsfor.NETDevelopersCreditsAbouttheAuthorAbouttheReviewerwww.PacktPub.com

eBooks,discountoffers,andmoreWhysubscribe?

PrefaceWhatthisbookcoversWhatyouneedforthisbookWhothisbookisforConventionsReaderfeedbackCustomersupport

DownloadingtheexamplecodeDownloadingthecolorimagesofthisbookErrataPiracyQuestions

1.WhyNode.js?WhatisNode.js?

UnderstandingtheNode.jsexecutionmodelNon-blockingEvent-drivenSingle-threaded

IntroducingtheNode.jsecosystemWhyJavaScript?

AclearcanvasFunctionalnatureAbrightfuture

WhentouseNode.jsWritingwebapplicationsIdentifyingotherusecasesWhynow?

Summary2.GettingStartedwithNode.js

InstallingandrunningNode.jsChoosinganeditorUsinganapplicationframework

GettingstartedwithExpressExploringourExpressapplication

UnderstandingExpressroutesandviewsUsingnodemonforautomaticrestartsCreatingmodularapplicationswithExpressBootstrappinganExpressapplicationUnderstandingExpressmiddleware

ImplementingerrorhandlingUsingExpressmiddleware

Summary3.AJavaScriptPrimer

IntroducingJavaScripttypesJavaScriptprimitivetypes

Functionalobject-orientedprogrammingFunctionalprogramminginJavaScriptUnderstandingscopesinJavaScript

StrictmodeObject-orientedprogramminginJavaScript

ProgrammingwithoutclassesCreatingobjectswiththenewkeyword

ProgrammingwithclassesClass-basedinheritance

Summary4.IntroducingNode.jsModules

OrganizingyourcodebaseJavaScriptmodulesystems

CreatingmodulesinNode.jsDeclaringamodulewithanameanditsownscopeDefiningfunctionalityprovidedbythemoduleImportingamoduleintoanotherscript

Definingadirectory-levelmoduleImplementinganExpressmiddlewaremoduleSummary

5.CreatingDynamicWebsitesHandlinguser-submitteddataCommunicatingviaAjaxImplementingotherdataoperations

ListingdatainviewsIssuingadeleterequestfromtheclientSplittingupExpressviewsusingpartials

Summary6.TestingNode.jsApplications

WritingasimpletestinNode.jsStructuringthecodebasefortestsWritingBDD-styletestswithMocha

Resettingstatebetweentests

UsingChaiforassertionsCreatingtestdoubles

CreatingtestdoublesusingSinon.JSTestinganExpressapplication

SimplifyingtestsusingSuperAgentFull-stacktestingwithPhantomJSSummary

7.SettingupanAutomatedBuildSettingupanintegrationserver

SettingupapublicGitHubrepositoryBuildingaprojectonTravisCI

AutomatingthebuildprocesswithGulpRunningtestsusingGulp

CheckingcodestylewithESLintAutomaticallyfixingissuesinESLintRunningESLintfromGulp

GatheringcodecoveragestatisticsRunningintegrationtestsfromGulpSummary

8.MasteringAsynchronicityUsingthecallbackpatternforasynchronouscode

ExposingthecallbackpatternConsumingasynchronousinterfaces

WritingcleanerasynchronouscodeusingpromisesImplementingpromise-basedasynchronouscode

ConsumingthepromisepatternParallelisingoperationsusingpromises

CombiningasynchronousprogrammingpatternsSummary

9.PersistingDataIntroducingMongoDB

WhychooseMongoDB?ObjectmodelingJavaScriptScalability

GettingstartedwithMongoDBUsingtheMongoDBshell

UsingMongoDBwithExpressPersistingobjectswithMongooseIsolatingpersistencecodeDependencyinjectioninNode.jsProvidingdependenciesRunningdatabaseintegrationtestsonTravisCI

IntroducingRedis

WhyuseRedis?InstallingRedisUsingRedisasakey-valuestoreStoringstructureddatainRedis

BuildingauserrankingsystemwithRedisUsingRedisfromNode.js

Testingwithredis-jsImplementinguserrankingswithRedisMakinguseoftheusersservice

AnoteonsecuritySummary

10.CreatingReal-timeWebAppsUnderstandingoptionsforreal-timecommunicationIntroducingSocket.IO

ImplementingachatroomwithSocket.IOScalingreal-timeNode.jsapplications

UsingRedisasabackendIntegratingSocket.IOwithExpressDirectingSocket.IOmessagesTestingSocket.IOapplicationsOrganizingSocket.IOapplications

Exposingreal-timeupdatestothemodelOrganizingSocket.IOapplicationsusingnamespacesPartitioningSocket.IOclientsusingrooms

Summary11.DeployingNode.jsApplications

WorkingwithHerokuSettingupaHerokuaccountandtoolingRunninganapplicationlocallywithHerokuDeployinganapplicationtoHerokuWorkingwithHerokulogs,config,andservices

SettingupMongoDBSettingupRedis

DeployingfromTravisCISettingencryptedTravisCIenvironmentvariables

InstallingRubyCreatinganencryptedenvironmentvariable

FurtherresourcesSummary

12.AuthenticationinNode.jsIntroducingPassport

ChoosinganauthenticationstrategyUnderstandingthird-partyauthentication

UsingExpresssessions

SpecifyingasessionsecretDecidingwhenthesessiongetssavedUsingalternativesessionstoresUsingsessionmiddleware

ImplementingsocialloginSettingupaTwitterapplicationConfiguringPassportPersistinguserdatawithRedisConfiguringPassportwithpersistenceHidingfunctionalityfromunauthenticatedusersIntegrationtestingwithPassport

AllowinguserstologoutAddingotherloginprovidersSummary

13.CreatingJavaScriptPackagesWritinguniversalmodules

ComparingNode.jsandRequireJSSupportingthebrowserenvironmentUsingAMDmoduleswithRequireJSIsomorphicJavaScript

WritingnpmpackagesDefiningannpmpackage

PublishingapackagetonpmRunningautomatedclientsonthewebReleasingastandalonetooltonpm

UsingNode.jsmodulesinthebrowserControllingBrowserify'soutput

Summary14.Node.jsandBeyond

UnderstandingNode.jsversioningAbriefhistoryofNode.jsIntroducingtheNode.jsLTSschedule

UnderstandingECMAScriptversioningExploringECMAScript2015

UnderstandingES2015modulesUsingsyntaximprovementsfromES2015

Thefor...ofloopThespreadoperatorandrestparametersDestructuringassignment

IntroducinggeneratorsIntroducingECMAScript2016GoingbeyondJavaScript

Exploringcompile-to-JavaScriptlanguagesTypeScript

CoffeeScriptAndbeyond...

IntroducingatrueassemblylanguageforthewebUnderstandingasm.jsUnderstandingWebAssembly

JavaScriptandASP.NETExploring.NETCore

Definingprojectstructurein.NETCoreManagingdependenciesin.NETCoreBuildingwebapplicationsinASP.NETCore

IntegrationwithJavaScriptServer-sideJavaScriptintegrationwith.NET

SummaryIndex

LearningNode.jsfor.NETDevelopers

LearningNode.jsfor.NETDevelopersCopyright©2016PacktPublishing

Allrightsreserved.Nopartofthisbookmaybereproduced,storedinaretrievalsystem,ortransmittedinanyformorbyanymeans,withoutthepriorwrittenpermissionofthepublisher,exceptinthecaseofbriefquotationsembeddedincriticalarticlesorreviews.

Everyefforthasbeenmadeinthepreparationofthisbooktoensuretheaccuracyoftheinformationpresented.However,theinformationcontainedinthisbookissoldwithoutwarranty,eitherexpressorimplied.Neithertheauthor,norPacktPublishing,anditsdealersanddistributorswillbeheldliableforanydamagescausedorallegedtobecauseddirectlyorindirectlybythisbook.

PacktPublishinghasendeavoredtoprovidetrademarkinformationaboutallofthecompaniesandproductsmentionedinthisbookbytheappropriateuseofcapitals.However,PacktPublishingcannotguaranteetheaccuracyofthisinformation.

Firstpublished:June2016

Productionreference:1170616

PublishedbyPacktPublishingLtd.

LiveryPlace

35LiveryStreet

BirminghamB32PB,UK.

ISBN978-1-78528-009-2

www.packtpub.com

CreditsAuthor

HarryCummings

Reviewer

DavidSimons

CommissioningEditor

KunalParikh

AcquisitionEditor

RahulNair

ContentDevelopmentEditor

TrushaShriyan

TechnicalEditor

JayeshSonawane

CopyEditor

SafisEditing

ProjectCoordinator

KinjalBari

Proofreader

SafisEditing

Indexer

MariammalChettiyar

Graphics

DishaHaria

ProductionCoordinator

NileshMohite

CoverWork

NileshMohite

AbouttheAuthorHarryCummingshasbeenworkinginsoftwaredevelopmentfor8years,andforthepastfewyears,hehasperformedtheroleoftechnicalleadacrossavarietyofprojectsforvariedclients.Hehas,inthepast,alsoworkedasadeveloper,projectmanager,andconsultant.Thisgiveshimanexcellentall-roundviewoftheroleofatechnicalleadanditsrelationshipwithotherrolesaswellasinsightintoeverystageofprojectdelivery,frominitialanalysistolong-termmaintenance.

HarryhasextensiveexperienceinC#/.NET,JavaandScala,andJavaScript/Node.js.Hecontinuestoworkdirectlywiththesetechnologiesonaregularbasisintheteamsthatheleads.Hisbroaderinterestsandexpertiselieinsharingandnurturingsoftwaredevelopmentbestpracticesthroughtrainingandmentoring.HehasappearedatconferencessuchasNDCLondonandSDDConf,speakingaboutdiversetopics,rangingfromintroductoryNode.jsthroughtoautomatedteststrategiesandlong-termprojectmaintainability.

AbouttheReviewerDavidSimonsisaLondon-basedsoftwareconsultant.Heisfamiliarwithawiderangeoftools,havinghelpedclientssuchastheBBCandNewsInternationaldeliverwebsolutionsinarangeoflanguages,including.NET,Java,andfull-stackJavaScript.Heshareshisinsightsaroundtheseandhisbackgroundinstatisticsresearchatarangeofconferences,includingNDCandJSConf.

Asof2016,heworkswithLondon-basedconsultancyGraphAwaretoadvocateandconsultontheuseofgraphdatabasesinmodernapplications.

www.PacktPub.com

eBooks,discountoffers,andmoreDidyouknowthatPacktofferseBookversionsofeverybookpublished,withPDFandePubfilesavailable?YoucanupgradetotheeBookversionatwww.PacktPub.comandasaprintbookcustomer,youareentitledtoadiscountontheeBookcopy.Getintouchwithusat<[email protected]>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

PrefaceThepurposeofthisbookistohelp.NETorJavadevelopersmaketheleaptoNode.js.Youmayhavesomewebdevelopmentexperience,andperhapsyou'vewrittensomebrowser-basedJavaScriptinthepast.ItmightnotbeobviouswhyanyonewouldwanttotakeJavaScriptoutofthebrowseranduseitforserver-sidedevelopment.However,thisisexactlywhatNode.jsdoes.What'smore,Node.jshasbeenaroundforlongenoughnowtohavematuredasaplatform,andhassustaineditsimpressivegrowthinpopularitywellbeyondanyperiodthatcouldbeattributedtoinitialhypeoveranewtechnology.

ThefirstobjectiveofthisbookthenistoexplainwhyNode.jsisacompellingtechnologythat'sworthlearningmoreabout.ThefirstfewchaptersintroduceNode.jswiththisinmind,quicklygetyouupandrunningwithNode.js,andprovideanimportant(re)introductiontotheJavaScriptlanguagetosetyouontherighttrack.

ThemainpartofthisbookwillthentakeyouthroughaworkedexampleofbuildingupaNode.jsweb-applicationstepbystep.Intheprocess,we'llillustratealltheimportanttoolsandtechniquesrequiredforreal-worlddevelopmentprojectsinNode.js.TheaimistomakethemostofyourexistingdevelopmentexpertisetoallowyoutoquicklyreachthesamelevelofbestpracticesandprofessionalismwithNode.js.

ThefinalchaptersofthebookshowhowtouseNode.jsforotherpurposesoutsideofwebapplicationsandhowtocontinuelearningNode.jsandexploringtheecosystemaroundit.We'llalsoseehowyoucanuseNode.jsalongside.NETandbenefitfromapplyingyourprogrammingskillsacrossbothtechnologies.

WhatthisbookcoversChapter1,WhyNode.js?,introducesNode.jsasaprogrammingplatform.ItcoverstheexecutionmodelofNode.js,particularlyhowitdiffersfrom.NETandJava,andtheusecasesinwhichthesedifferencesbecomestrengths.ThischapteralsodiscussesthesuitabilityofJavaScriptasadevelopmentlanguage.

Chapter2,GettingStartedwithNode.js,divesstraightintocreatingaNode.jsapplication.Inthischapter,youwillinstallNode.js,chooseacodeeditor,andsetupaminimalwebapplicationproject.You'llalsolearnsomeimportantcommand-linetoolsforworkingwithNode.js.

Chapter3,AJavaScriptPrimer,introducesthemostimportantthingstoknowwhenprogramminginJavaScript.ItdescribestheJavaScripttypesystemanditsparticularflavoroffunctionalobject-orientedprogramming,includingprototype-basedinheritance.ThischapteralsocoversafewkeygotchasandJavaScriptlanguagequirks.

Chapter4,IntroducingNode.jsModules,explainshowtostructureJavaScriptapplicationsusingmodules.ItintroducestheNode.jsmodulesystemandshowsyouhowtousethistoorganiseyourapplication'scode.

Chapter5,CreatingDynamicWebsites,expandsontheexamplesfromthepreviouschaptertobuildafunctioningwebapplication.You'lladdaJSONAPIanddynamicviewstoyourapplicationandcommunicatebetweentheclientandserverusingAjax.

Chapter6,TestingNode.jsApplications,showsyouhowtowriteautomatedtestsinJavaScriptandNode.js.ItintroducesanumberoftoolsandlibrariesforwritingandrunningtestsinJavaScript,andguidesyouthroughwritingavarietyofunittestsandintegrationtestsforyourapplication.

Chapter7,SettingupanAutomatedBuild,coversbuildautomationandcontinuousintegrationinNode.js.You'llsetupaCIserverandtaskrunnerforyourapplication,addingautomatedtaskstoruntests,executestaticanalysis,andassesscodecoverage.

Chapter8,MasteringAsynchronicity,introducesdifferentpatternsforasynchronousprogramminginJavaScript.You'llapplythesetoyourownapplicationandmakethemostofJavaScriptlanguagefeaturesandlibrariesforsimplifyingasynchronouscode.

Chapter9,PersistingData,explainspersistentdatastoresthatcanbeusedwithNode.js.ItintroducesMongoDBandRedis,explainingtheirdifferentdatamodelsandtheirusecases.You'llintegratebothofthesedatastoreswithyourNode.jsapplication.

Chapter10,CreatingReal-timeWebApps,showshowtoimplementreal-timetwo-waycommunicationbetweentheclientandtheserver.You'llusetheSocket.IOlibrarytoaddreal-timefunctionalityintoyourapplication.You'llalsoseehowtowritetestsforthisfunctionalityand

howtowritescalablereal-timeapplicationsusingRedisasabackend.

Chapter11,DeployingNode.jsApplications,demonstrateshowtogetaNode.jsapplicationontotheWeb.You'lldeployyourapplicationtoafreecloud-hostingprovider.You'llseehowtoconfiguredatastoresandhowtouseremoteserverlogsfordebugging.

Chapter12,AuthenticationinNode.js,coversauthenticationforNode.jswebapplications.You'llimplementauthenticationusingthird-partyproviders,integratethiswithyourapplication,andshowdifferentcontenttologged-inandlogged-outusers.

Chapter13,CreatingJavaScriptPackages,explainshowtocreatestandaloneJavaScriptpackagesforusebyothers.You'llseehowtowriteuniversalJavaScriptlibrariesthatcanrunonboththeclientandtheserver,andhowtowriteastandalonecommand-lineapplicationusingNode.js.

Chapter14,Node.jsandBeyond,putsthecontentofthisbookinawidercontext.ItexplainshowNode.jsandJavaScriptarecontinuingtoevolve,soyoucanbepreparedforandtakeadvantageofupcomingchanges.ItcoverssomealternativeprogramminglanguagesforNode.jsandtheWeb,andhowtheserelatetoJavaScript.ItdiscusseshowsomeoftheprinciplesfromNode.jscanbeappliedto.NETprogramming,andillustrateshowtheseareparticularlyvisiblein.NETCore(thenewversionof.NET).ItalsoshowshowyoucanuseNode.jstogetherwith.NETtogainthebestofbothworlds.

WhatyouneedforthisbookAllofthetoolsandservicesusedinthisbookareavailableforfreeonline.Mostoftheworkedexamplesrequireanactivewebconnectionatsomepoint.Togetstarted,youneednothingmorethanaconsole,awebbrowser,andpermissiontoinstallnewsoftwareonyourmachine.Tosupportdeveloperscomingfroma.NETbackground,someoftheconsolelistingsorexamplestepsinthisbookuseWindowsconventions(forexample,backslashesinpaths).NoneoftheexamplesdependonWindowsspecificallythough.YoucanworkthroughthisbookonWindows,MacOSX,orLinux.

WhothisbookisforThisbookisfor.NETorJavadeveloperswhoareinterestedinlearningNode.js.NopriorexperiencewithNode.jsisexpected.Youmighthavewrittensomeclient-sideJavaScriptbefore,butthisisnotrequired.ThemainworkedexampleinthisbookisaNode.jswebapplication.Webdevelopmentexperiencein.NETorJavawillbehelpful,butit'snotnecessarytohaveexperiencewithanyparticularapplicationlibraryorframework.

ConventionsInthisbook,youwillfindanumberoftextstylesthatdistinguishbetweendifferentkindsofinformation.Herearesomeexamplesofthesestylesandanexplanationoftheirmeaning.

Codewordsintext,databasetablenames,foldernames,filenames,fileextensions,pathnames,dummyURLs,userinput,andTwitterhandlesareshownasfollows:"ES2015introducestheletkeywordfordeclaringvariables."

Ablockofcodeissetasfollows:

<!DOCTYPEhtml>

<html>

<head>

<title>{{title}}</title>

<linkrel='stylesheet'href='/stylesheets/style.css'/>

</head>

<body>

<h1>{{title}}</h1>

<p>Welcometo{{title}}</p>

</body>

</html>

Whenwewishtodrawyourattentiontoaparticularpartofacodeblock,therelevantlinesoritemsaresetinbold:

/*GEThomepage.*/

router.get('/',function(req,res,next){

res.render('index',{title:'Express',name:'World'});

});

Anycommand-lineinputoroutputiswrittenasfollows:

>npminstall–gnodemon

Newtermsandimportantwordsareshowninbold.Wordsthatyouseeonthescreen,forexample,inmenusordialogboxes,appearinthetextlikethis:"ClickingtheNextbuttonmovesyoutothenextscreen."

Note

Warningsorimportantnotesappearinaboxlikethis.

Tip

Tipsandtricksappearlikethis.

ReaderfeedbackFeedbackfromourreadersisalwayswelcome.Letusknowwhatyouthinkaboutthisbook—whatyoulikedordisliked.Readerfeedbackisimportantforusasithelpsusdeveloptitlesthatyouwillreallygetthemostoutof.

Tosendusgeneralfeedback,simplye-mail<[email protected]>,andmentionthebook'stitleinthesubjectofyourmessage.

Ifthereisatopicthatyouhaveexpertiseinandyouareinterestedineitherwritingorcontributingtoabook,seeourauthorguideatwww.packtpub.com/authors.

CustomersupportNowthatyouaretheproudownerofaPacktbook,wehaveanumberofthingstohelpyoutogetthemostfromyourpurchase.

DownloadingtheexamplecodeYoucandownloadtheexamplecodefilesforthisbookfromhttps://github.com/NodeJsForDevelopersandalsofromyouraccountathttp://www.packtpub.com.Ifyoupurchasedthisbookelsewhere,youcanvisithttp://www.packtpub.com/supportandregistertohavethefilese-maileddirectlytoyou.

Youcandownloadthecodefilesbyfollowingthesesteps:

1. Loginorregistertoourwebsiteusingyoure-mailaddressandpassword.2. HoverthemousepointerontheSUPPORTtabatthetop.3. ClickonCodeDownloads&Errata.4. EnterthenameofthebookintheSearchbox.5. Selectthebookforwhichyou'relookingtodownloadthecodefiles.6. Choosefromthedrop-downmenuwhereyoupurchasedthisbookfrom.7. ClickonCodeDownload.

YoucanalsodownloadthecodefilesbyclickingontheCodeFilesbuttononthebook'swebpageatthePacktPublishingwebsite.Thispagecanbeaccessedbyenteringthebook'snameintheSearchbox.PleasenotethatyouneedtobeloggedintoyourPacktaccount.

Oncethefileisdownloaded,pleasemakesurethatyouunziporextractthefolderusingthelatestversionof:

WinRAR/7-ZipforWindowsZipeg/iZip/UnRarXforMac7-Zip/PeaZipforLinux

DownloadingthecolorimagesofthisbookWealsoprovideyouwithaPDFfilethathascolorimagesofthescreenshots/diagramsusedinthisbook.Thecolorimageswillhelpyoubetterunderstandthechangesintheoutput.Youcandownloadthisfilefromhttp://www.packtpub.com/sites/default/files/downloads/LearningNodejsForNETDevelopers_ColorImages.pdf

ErrataAlthoughwehavetakeneverycaretoensuretheaccuracyofourcontent,mistakesdohappen.Ifyoufindamistakeinoneofourbooks—maybeamistakeinthetextorthecode—wewouldbegratefulifyoucouldreportthistous.Bydoingso,youcansaveotherreadersfromfrustrationandhelpusimprovesubsequentversionsofthisbook.Ifyoufindanyerrata,pleasereportthembyvisitinghttp://www.packtpub.com/submit-errata,selectingyourbook,clickingontheErrataSubmissionFormlink,andenteringthedetailsofyourerrata.Onceyourerrataareverified,yoursubmissionwillbeacceptedandtheerratawillbeuploadedtoourwebsiteoraddedtoanylistofexistingerrataundertheErratasectionofthattitle.

Toviewthepreviouslysubmittederrata,gotohttps://www.packtpub.com/books/content/supportandenterthenameofthebookinthesearchfield.TherequiredinformationwillappearundertheErratasection.

PiracyPiracyofcopyrightedmaterialontheInternetisanongoingproblemacrossallmedia.AtPackt,wetaketheprotectionofourcopyrightandlicensesveryseriously.IfyoucomeacrossanyillegalcopiesofourworksinanyformontheInternet,pleaseprovideuswiththelocationaddressorwebsitenameimmediatelysothatwecanpursuearemedy.

Pleasecontactusat<[email protected]>withalinktothesuspectedpiratedmaterial.

Weappreciateyourhelpinprotectingourauthorsandourabilitytobringyouvaluablecontent.

QuestionsIfyouhaveaproblemwithanyaspectofthisbook,youcancontactusat<[email protected]>,andwewilldoourbesttoaddresstheproblem.

Chapter1.WhyNode.js?Node.jsisstillrelativelynewcomparedtoplatformssuchas.NETandJava,buthasbecomeverypopularinashorttime,andhasevenstartedinfluencingtheseplatforms.Thisisthankstoitsdistinctiveprogrammingmodel,extensiveecosystem,andpowerfultooling.

ThesefactorsmakeNode.jsacompellingalternativetootherplatforms.Theycanalsomakeitintimidating.Itsdistinctiveprogrammingmodelmayseemquitealiencomparedtootherplatforms.Thesheerrangeofavailablelibrariesandtoolscanbebewildering.

ThisbookwillguideyouthroughNode.jssoyoucanstartusingitinyourapplications.ItwillhelpyoutounderstandNode.js,navigateitsecosystem,andleverageyourexistingdevelopmentskillsinthisnewenvironment.

Inthischapter,wewillcoverthefollowingtopics:

IntroducingtheNode.jsplatformSeeinghowitsexecutionmodelworksExploringtheNode.jsecosystemLookingatJavaScriptasalanguagechoiceConsideringtherangeofusecasesforNode.js

WhatisNode.js?Node.jsconsistsofaJavaScriptenginetogetherwithlow-levelAPIsforcoreserver-sidefunctionality.TheexecutionengineisthesameV8enginedevelopedfortheChromewebbrowser.Node.jstakesthisengineandembedsitinastandaloneapplicationthatcanrunJavaScriptoutsidethebrowser.

InNode.js,thestandardAPIsfoundinbrowserstosupportclient-sidewebdevelopment,suchastheDocumentObjectModel(DOM)andXMLHttpRequest,arenotpresent.Instead,thereareAPIstosupportgeneral-purposeapplicationdevelopment.ThesecoreAPIscoverlow-levelfunctionalitysuchasthefollowing:

NetworkingandsecurityAccessingthefilesystemDefiningandrequiringmodulesRaisingandconsumingeventsHandlingbinarydatastreamsCompressionUTF-8supportRetrievingbasicinformationabouttheOSManagingchildprocesses

SomeoftheseAPIsmayalreadybefamiliarfromdevelopingclient-sideJavaScript.Forexample,theTimersAPIexposesthefamiliarsetTimeoutandsetIntervalfunctions.

Node.jsalsoprovidesseveraltoolstohelpwiththedevelopmentprocess.Theseincludeconsolelogging,debugging,aRead-Eval-PrintLoop(REPL)(orinteractiveconsole),andbasicassertionsfortesting.

UnderstandingtheNode.jsexecutionmodelTheexecutionmodelofNode.jsfollowsthatofJavaScriptinthebrowser.Itisquitedifferentfromthatofmostgeneral-purposeprogrammingplatforms.

Statedformally,Node.jshasasingle-threaded,non-blocking,event-drivenexecutionmodel.Wewilldefineeachofthesetermsinthissection.

Non-blocking

Putsimply,Node.jsrecognizesthatmanyprogrammesspendmostoftheirtimewaitingforotherthingstohappen,forexample,slowI/Ooperationssuchasdiskaccessandnetworkrequests.

Node.jsaddressesthisbymakingtheseoperationsnon-blocking.Thismeansthatprogramexecutioncancontinuewhiletheyhappen.Forexample,thefilesystemAPI'sstatfunctionforretrievingstatisticsaboutafilemaybecalledasfollows:

fs.stat('/hello/world',function(error,stats){

console.log('Filelastupdatedat:'+stats.mtime);

});

Twoargumentsarepassedtothefs.statfunction:thenameofthefilethatweareinterestedin,andacallbackfunction.Thefs.statcallreturnsimmediately,returningcontrolofexecutiontothecurrentthreadbutnotreturningavalue.Iftherearefurthercommandsfollowingthefs.statcall,thesewillthenbeexecuted.Otherwise,thethreadisreleasedtoperformotherwork.Thecallbackfunctionisinvoked(thatis'calledback')onlyaftertheruntimehasfinishedcommunicatingwiththefilesystem.Theresultofthefilesystemoperationispassedintothecallbackfunction.

Thisnon-blockingapproachisalsocalledasynchronousprogramming.Otherplatformssupportthis(forexample,C#'sasync/awaitkeywordsand.NET'sTaskParallelLibrary).However,itisbakedintoNode.jsinawaythatmakesitsimpleandnaturaltouse.AsynchronousAPImethodsareallcalledinthesamewayasfs.stat.Theyalltakeacallbackfunctionthatgetspassederrorandresultarguments.

Event-driven

Theevent-drivennatureofNode.jsdescribeshowoperationsarescheduled.Intypicalproceduralenvironments,aprogramhasanentrypointthatexecutesasetofcommandsuntilcompletion,orentersaloopandperformssomeprocessingoneachiteration.

Node.jshasabuilt-ineventloop,whichisn'texposedtothedeveloper.Itisthejoboftheeventlooptodecidewhichpieceofcodetoexecutenext.Typically,thiswillbeacallbackfunctionthatisreadytoruninresponsetosomeotherevent.Forexample,afilesystemoperationmayhavecompleted,atimeoutmayhaveexpired,oranewnetworkrequestmayhavearrived.

Thisbuilt-ineventloopsimplifiesasynchronousprogrammingbyprovidingaconsistentapproach

andavoidingtheneedforapplicationstomanagetheirownscheduling.

Single-threaded

Thesingle-threadednatureofNode.jssimplymeansthatthereisonlyonethreadofexecutionineachprocess.Also,eachpieceofcodeisguaranteedtoruntocompletionwithoutbeinginterruptedbyotheroperations.Thisgreatlysimplifiesdevelopmentandmakesprogramseasiertoreasonabout.Itremovesthepossibilityforarangeofconcurrencyissues.Forexample,itisnotnecessarytosynchronize/lockaccesstosharedin-processstateasitisinJavaor.NET.Aprocesscan'tdeadlockitselforcreateraceconditionswithinitsowncode.Single-threadedprogrammingisonlyfeasibleifthethreadnevergetsblockedwaitingforlong-runningworktocomplete.Thus,thissimplifiedprogrammingmodelismadepossiblebythenon-blockingnatureofNode.js.

IntroducingtheNode.jsecosystemThebuilt-inNode.jsAPIsprovidealow-levelcoreforcreatingapplications.ApplicationstypicallyonlyuseasmallnumberoftheseAPIsdirectly.Theyoftenusethird-partylibrarymodulesthatprovidehigher-levelabstractionsforapplicationdevelopment.

Node.jshasitsownpackagemanager,npm.Thisissimilarto.NET'sNuGetorthepackagemanagementaspectsofJava'sMaven.ApplicationsspecifytheirdependenciesinasimpleJSONfile.

Thenpmregistryprovidesacentralrepositoryforpackages.Thisregistryhasgrownrapidlyandisalreadymuchlarger(intermsofnumberofavailablepackages)thanthecorrespondingrepositoriesforotherplatforms(seehttp://www.modulecounts.com/).Therearehundredsofthousandsofpackagesavailable,providingavastarrayoffunctionality.

Thenpmcommandlinetoolcanbeusedtodownloadpackagesandinstallnewones.Librarydependenciesareinstalledlocallytoeachapplication.Somepackagesprovidecommand-linetools,whichmaybeinstalledgloballyratherthanunderaspecificproject.

Manyframeworksavailableonnpmaresplitintoasmallextensiblecoreandanumberofcomposablemodules.Thisapproachmakesiteasytounderstandthelibrariesonwhichyourapplicationdepends,avoidingtheneedtoreasonaboutcomplexheavyweightframeworks.

Theconsistencyofcallingnon-blocking(asynchronous)APImethodsinNode.jscarriesthroughtoitsthird-partylibraries.Thisconsistencymakesiteasytobuildapplicationsthatareasynchronousthroughout.

WhyJavaScript?JavaScriptisalanguagethatcanseemunintuitivecomparedtootherpopularobject-oriented(OO)languages.Italsohasanumberofquirksandflawsthathavedrawncriticism(andoccasionalridicule).Itmightthenseemasurprisingchoiceoflanguageforanewprogrammingplatform.ThissectiondiscussesthefactorsthatmakeJavaScriptamoreappealingchoice.

AclearcanvasThesizeandcomplexityofJavaScriptispartofitsappeal.Thecorelanguageitself,whichdoesn'tincludeAPIssuchastheDOM,issmallandsimple.ThismakesiteasyforNode.jstoestablishitsownstylesandconventions.

ThenewAPIsprovidedbyNode.jsandtheconsistentapproachtoasynchronousprogrammingwouldn'tbepossibleinamorecomplexlanguagewithalargerpre-existingstandardclasslibrary.

FunctionalnatureJavaScriptwasfirstbuiltasaprogramminglanguageforclient-sidefunctionalityinthebrowser.Thismightnotmakeitanobviouschoiceforgeneral-purposeprogramming.

Infact,thesetwousecasesdohavesomethingimportantincommon.Userinterfacecodeisnaturallyevent-driven(forexample,bindingeventhandlerstobuttonclicks).Node.jsmakesthisavirtuebyapplyinganevent-drivenapproachtogeneral-purposeprogramming.

JavaScriptsupportsfunctionsasfirst-classobjects.Thismeansit'seasytocreatefunctionsdynamicallyandpassaroundreferencestothem.Thisfitsinwellwiththeasynchronous,non-blockingapproachofNode.js.Inparticular,it'seasytoexposeanduseAPIsbasedaroundcallbackfunctions.

AbrightfutureJavaScripthasreceivedalotofattentioninthelastseveralyearsasithasbecomemorewidelyusedforprovidingrichfunctionalityontheWeb.BrowservendorshaveputahugeamountofengineeringeffortintoimprovingtheperformanceofJavaScript.Node.jsbenefitsfromthisdirectlyviaitsuseofChrome'sV8engine.

TheJavaScriptlanguageitselfisundergoingsomemajorchangesforthebetter.TheECMAScript2015standard(previouslyknownasES6)representsthemostsignificantrevisionofthelanguageinitshistory.Itintroducesfeaturesthatmakethelanguagemoreintuitiveandlessverbose.ItalsoaddressesflawsthatJavaScripthasbeencriticizedforinthepast,removinggotchasandmakingprogramseasiertoreasonabout.

WhentouseNode.jsAsdiscussedearlierinthischapter,Node.jsrecognizesthatI/Oisabottleneckformanyapplications.Onmostprogrammingplatforms,threadswillwastetimeblockingonI/Ooperations.Thereareapproachesdeveloperscantaketoavoidthis,buttheseallinvolveaddingsomecomplexitytotheircode.InNode.js,theplatformitselfprovidesacompletelynaturalapproach.

WritingwebapplicationsTheflagshipusecaseforNode.jsisbuildingwebapplications.Theseareinherentlyevent-drivenasmostorallprocessingtakesplaceinresponsetoHTTPrequests.Also,manywebsitesdolittlecomputationalheavy-liftingoftheirown.TheytendtoperformalotofI/Ooperations:

StreamingrequestsfromtheclientTalkingtoadatabase,locallyoroverthenetworkPullingindatafromremoteAPIsoverthenetworkReadingfilesfromdisktosendbacktotheclient

ThesefactorsmakeI/Ooperationsalikelybottleneckforwebapplications.Thenon-blockingprogrammingmodelofNode.jsallowswebapplicationstomakethemostofasinglethread.AssoonasanyoftheseI/Ooperationsstarts,thethreadisimmediatelyfreetopickupandstartprocessinganotherrequest.ProcessingofeachrequestcontinuesviaasynchronouscallbackswhenI/Ooperationscomplete.Theprocessingthreadisonlykickingoffandlinkingtogethertheseoperations,neverwaitingforthemtocomplete.ThisallowsNode.jstohandleamuchhigherrateofrequestsperthreadthanotherplatforms.Youcanalsostillmakeuseofmultiplethreads(forexample,onmulti-coreCPUs)bysimplyrunningmultipleinstancesoftheNode.jsprocess.

IdentifyingotherusecasesThereareofcoursesomeapplicationsthatdon'tperformmuchI/OandaremorelikelytobeCPUbound.Node.jswouldbelesssuitableforcomputationally-intensiveapplications.Programsthatdoalotofprocessingofin-memorydataarelessconcernedaboutI/O.

WebapplicationsarenottheonlyI/O-heavyapplicationsthough.OtherclassesofprogramthatcouldbeagoodcandidateforNode.jsincludethefollowing:

ToolsthatmanipulatelargeamountsofdataondiskSupervisorprogramscoordinatingothersoftwareorhardwareNon-browserGUIapplicationsthatneedtorespondtouserinput

Node.jsisespeciallysuitableforglueapplicationsthatpulltogetherfunctionalityfromotherremoteservices.Theincreasingpopularityofmicroservicesasanarchitecturalpatternmakesthiskindofapplicationmorecommon.

Whynow?Node.jshasbeenaroundforseveralyears,butnowistheperfecttimetostartusingitifyouhaven'talready.

ThereleaseofNode.jsv4towardstheendof2015consolidatedtheproject'sgovernancemodelandheraldsNode.jscomingtomaturity.ItalsoallowstheprojecttokeepmoreuptodatewiththeV8engine.ThismeansthatNode.jscanbenefitmoredirectlyfromongoingdevelopmentonV8.Forexample,securityandperformanceimprovementstoV8willnowmaketheirwayintoNode.jsmuchsooner.

Asdiscussedearlierinthischapter,thereleaseoftheECMAScript2015standardmakesJavaScriptamuchmoreappealinglanguage.ItpullsinusefulfeaturesfromotherpopularOOlanguagesandresolvesanumberoflong-standingflawsinJavaScript.

Meanwhile,theecosystemofthirdpartylibrariesandtoolsaroundNode.jsandJavaScriptcontinuestogrow.Node.jsistreatedasafirst-classcitizenbymajorhostingplatforms.CompaniessuchasGoogleandMicrosoftarealsothrowingtheirweightbehindJavaScriptandrelatedtechnologies.

SummaryInthischapter,wehaveunderstoodNode.jsanditsdistinctiveexecutionmodel,exploredthegrowingecosystemaroundNode.jsandJavaScript,seenthereasonsforJavaScriptasalanguagechoice,anddescribedthekindsofapplicationthatcanbenefitfromNode.js.

NowthatyouknowhowNode.jsworksandwhentouseit,it'stimetodiveinandgetourfirstNode.jsapplicationupandrunning.

Chapter2.GettingStartedwithNode.jsThischapterwillgetyouupandrunningwithNode.js.You'llseehowquickthiscanbeandhoweasyitistostartwritingwebapplications.You'llalsochooseadevelopmentenvironmentforworkingwithNode.js.Inthischapter,wewillcoverthefollowingtopics:

InstallingNode.jsWritingourfirstNode.jswebapplicationSettingupourdevelopmentenvironment

InstallingandrunningNode.jsToinstallNode.js,visithttps://nodejs.org,anddownloadandruntheinstallerpackageforthecurrentlyrecommendedversion.TheexamplesinthisbookarebasedonNode.jsv6,releasedinApril2016andsupportedthroughtoApril2018.

Afterinstallation,openupaconsolewindow(runcommandpromptonWindows,orterminalonMac)andtypenode.

ThisopenstheNode.jsREPL,whichworksliketheJavaScriptconsoleinbrowsers.Trytypinginafewcommandsandseetheoutput:

>functionsquare(x){returnx*x;}

undefined

>square(42)

1764

>newDate()

2016-05-02T16:08:41.915Z

>varfoo={bar:'baz'}

undefined

>typeoffoo

'object'

>foo.bar

'baz'

Nowlet'smakeuseofoneoftheNode.js-specificAPIstocreateanHTTPserver.TypethefollowingcommandsintotheREPL(theoutputofeachcommandisomittedfromthelistingbelowforbrevity):

>varlistener=function(request,response){response.end('HelloWorld!')}

>require('http').createServer(listener).listen(3000)

Nowtryvisitinghttp://localhost:3000inyourbrowser.Congratulations!Youhavewrittenyourfirstwebserver,injusttwolinesofcode.ThefirstlinedefinesacallbackfunctionforhandlingHTTPrequestsandreturningaresponse.ThesecondlinesetsupanewserverthatacceptsHTTPrequestsonport3000andinvokesourcallbackfunctionforeachrequest.

YoucanexittheNode.jsREPLbytypingprocess.exit().

ChoosinganeditorOfcourse,we'renotgoingtowriteallofourcodeinsidetheREPL.YoucanuseanytexteditororIDEyoulikeforwritingJavaScriptforNode.js.Ifyou'renotsurewhattouse,tryoneofthefollowing:

Atom(https://atom.io/)VisualStudioCode(https://code.visualstudio.com/)

Thesearebothfree,lightweightIDEsthatareactuallyimplementedinNode.js.TheyarebothavailableforWindows,Mac,andLinux.

ThecodelistingsintherestofthisbookwillbeJavaScriptsourcecodefiles,notcommandstobetypedintotheREPL.

UsinganapplicationframeworkTheserverwecreatedintheREPLusedthelow-levelHTTPmodulebuiltintoNode.js.ThisprovidesanAPIforcreatingaserverthatreadsdatafromrequestsandwritestoresponses.

Aswithotherprogrammingplatforms,thereareframeworksavailableprovidingmoreusefulhigh-levelabstractionsforwritingwebapplications.TheseincludethingssuchasURLroutingandtemplatingengines.ASP.NETMVC,RubyonRails,andSpringMVCareallexamplesofsuchframeworksondifferentplatforms.

Note

Examplecode

Ifyougetstuckatanypointinthisbook,youcanfollowalongwiththecodeathttps://github.com/NodeJsForDevelopers(thereisarepositoryforeachchapterandacommitforeachheadingthatintroducesanynewcode).

Inthisbook,we'llbeusingaframeworkcalledExpresstowriteawebapplicationinNode.js.ExpressisthemostpopularwebapplicationframeworkforNode.js.Itiswellsuitedtosmall-scaleapplicationssuchastheonewe'llbebuilding.Italsoprovidesagoodintroductiontoimportantconcepts.MostotherpopularNode.jswebapplicationframeworksareconceptuallysimilartoExpress,andseveralareactuallybuiltontopofit.

GettingstartedwithExpressTogetourExpress-basedapplicationstarted,we'llusenpmtoinstalltheexpress-generatorpackage,whichwillcreateaskeletonapplicationbasedonExpress.Runthefollowingcommandintheconsole(thatis,yourregularterminal,notinsidetheNode.jsREPL):

>npminstall-gexpress-generator@~4.x

The-goptioninstallstheExpressgeneratorglobally,soyoucanrunitfromanywhere.Thenextcommandwerunwillcreateanewfoldertocontainourapplicationcode,sorunthiscommandwhereveryouwantthisfoldertoreside:

>express--hoganchapter02

Note

Templatingengines

Expressoffersachoiceoftemplatingengines.We'llbeusingHogan,whichisanimplementationoftheMustachetemplatingengine.YoumayalreadybefamiliarwithMustachefromclient-sidelibraries.Don'tworryifnot,though.It'sverysimpletopickup.

Asyoucanseefromtheoutput,thissetsupaminimalstandardapplicationstructureforus.Nowrunthefollowingcommand(asinstructedbythegeneratoroutput)toinstallthemodulesonwhichourapplicationdepends:

>cdchapter02

>npminstall

ThegeneratorhascreatedaskeletonNode.jswebapplicationforus.Let'stryrunningthis:

>npmstart

Nowvisithttp://localhost:3000againandyou'llseetheExpresswelcomepageasshownhere:

ExploringourExpressapplicationLet'slookatthefoldersthattheExpressgeneratorcreatedforus:

node_modules:Thisfoldercontainsthethird-partypackagesthatourapplicationdependson,whichareinstalledwhenwerunnpminstall(itiscommontoexcludethisdirectoryfromsourcecontrol)public:Thisfoldercontainsthestaticassetsofourapplication:images,client-sideJavaScript,andCSSroutes:Thisfoldercontainsthelogicofourapplicationviews:Thisfoldercontainstheserver-sidetemplatesforourapplication

Therearealsosomefilesthataren'tcontainedinanyoftheprecedingfolders:

package.json:Thisfilecontainsmetadataaboutourapplicationusedbythenpminstallandnpmstartcommandsusedearlier.We'llexplorethisfilefurtherinChapter4,IntroducingNode.jsModules.app.js:Thisfileisthemainentrypointforourapplication,whichgluestogetheralloftheprecedingcomponentsandinitializesExpress.We'llgothroughthisfileinmoredetaillateroninthischapter.bin/www:ThisfileisaNode.jsscriptthatlaunchesourapplication.Thisisthescriptthatgetsexecutedwhenwerunnpmstart.

It'snotimportanttounderstandeverythinginthebin/wwwscriptatthispoint.However,notethatitusesthesamehttp.createServercallasintheREPLexamplebefore.Thistime,though,thelistenerargumentisnotasimplefunctionbutisourentireapplication(definedinapp.js).

UnderstandingExpressroutesandviewsRoutesinExpresscontainthelogicforhandlingrequestsandrenderingtheappropriateresponse.TheyhavesimilarresponsibilitiestocontrollersinMVCframeworkssuchasASP.NET,SpringMVC,orRubyonRails.

Theroutethatservesthepagewejustviewedinthebrowsercanbefoundatroutes/index.jsandlookslikethis:

varexpress=require('express');

varrouter=express.Router();

/*GEThomepage.*/

router.get('/',function(req,res,next){

res.render('index',{title:'Express'});

});

module.exports=router;

TherequirecallimportstheExpressmodule.WewilldiscusshowthisworksinmuchmoredetailinChapter4,IntroducingNode.jsModules.Fornow,thinkofitlikeausingorimportstatementin.NETorJava.Thecalltoexpress.Router()createsacontextunderwhichwecandefinenewroutes.Wewilldiscussthisinmoredetaillateroninthischapter(seeCreatingmodularapplicationswithExpress).Therouter.get()calladdsanewhandlertothiscontextforGETrequeststothepath'/'.

Thecallbackfunctiontakesarequestandresponseargument,similartothelistenerinour"HelloWorld!"serveratthebeginningofthischapter.However,therequestandresponseinthiscaseareobjectsprovidedbyExpress,withadditionalfunctionality.

Therenderfunctionallowsustorespondwithatemplate,whichisrenderedusingthedatawepasstoit.Thisistypicallythelastthingyouwilldoinaroute'scallbackfunction.Here,wepassanobjectcontainingthetitleExpresstotheviewtemplate.

Theviewtemplatecanbefoundatviews/index.hjsandlookslikethis:

<!DOCTYPEhtml>

<html>

<head>

<title>{{title}}</title>

<linkrel='stylesheet'href='/stylesheets/style.css'/>

</head>

<body>

<h1>{{title}}</h1>

<p>Welcometo{{title}}</p>

</body>

</html>

ThisisaHogantemplate.Asmentionedpreviously,HoganisanimplementationofMustache,a

verylightweighttemplatinglanguagethatlimitstheamountoflogicinviews.YoucanseethefullsyntaxofMustacheathttps://mustache.github.io/mustache.5.html.

OurtemplateisasimpleHTMLpagewithsomespecialtemplatetags.The{{title}}tagsarereplacedwiththetitlefieldfromthedatapassedinbytheroute.

Let'schangetheheadingintheviewtoincludeanameaswellasatitle.Itshouldlooklikethis:

<h1>Hello,{{name}}!</h1>

Tryreloadingthepageagain.Youshouldseethefollowing:

Wedon'thaveanameyet.That'sbecausethereisnonamefieldinourviewdata.Let'sfixthatbyeditingourroute:

varexpress=require('express');

varrouter=express.Router();

/*GEThomepage.*/

router.get('/',function(req,res,next){

res.render('index',{title:'Express',name:'World'});

});

module.exports=router;

Ifwerefreshourbrowseragainatthispoint,westillwon'tseethename.That'sbecauseourapplicationhasalreadyloadedourroute,sowon'tpickupthechange.

Gobacktoyourterminalandkilltherunningapplication.Startitagain(usingnpmstart)andreloadthepageinthebrowser.YoushouldnowseethetextHello,World!.

UsingnodemonforautomaticrestartsRestartingtheapplicationeverytimewemakeachangeisabittedious.Wecandobetterbyrunningourapplicationwithnodemon,whichwillautomaticallyrestarttheapplicationwheneverwemakeachange:

>npminstall-gnodemon

>nodemon

Tryupdatingtheroutes/index.jsfileagain(forexample,changethenamestringtoyourownname),thenrefreshthebrowser.Thistime,thechangeshouldappearwithoutyouneedingtomanuallystopandrestarttheapplication.Notethattheprocessisrestartedbynodemonthough,soifourapplicationstoredanyinternalstate,thiswouldbelost.

CreatingmodularapplicationswithExpressTofindouthowourroutegetscalledwhenarequestismade,weneedtolookattheapp.jsbootstrappingfile.Seethefollowingtwolines:

varroutes=require('./routes/index');

...

app.use('/',routes);

ThistellsExpresstousetheroutingcontextdefinedinroutes/index.jsforrequeststotherootpath('/').

Thereisasimilarcallsettinguparouteunderthe/userspath.Tryvisitingthispathinyourbrowser.Theroutethatrendersthisresponseisdefinedin/routes/users.js.

Notethattheroutein/routes/users.jsisalsoboundto'/',thesameastheroutein/routes/index.js.ThereasonthisworksisthatthesepathsareeachrelativetoaseparateRouterinstance,andtheinstancecreatedin/routes/users.jsismountedunderthe/userspathinapp.js.

Thismechanismmakesiteasytobuildlargeapplicationscomposedfromsmallermodules.YoucanthinkofitassimilartotheAreasfunctionalityinASP.NETMVC,orsimplyasanalternativestructuretoMVCcontrollersgroupingtogetheractionmethods.

BootstrappinganExpressapplicationLet'stakealookattherestoftheapp.jsfile.YourfilemightnotlookidenticaltothelistingsbelowduetominordifferencesinourversionsofExpress,butitwillcontainbroadlythesamesections.

Thevariousrequire()callsatthetopofthefileimportthemodulesusedbytheapplication,includingbuilt-inNode.jsmodules(HTTPandPath),third-partylibraries,andtheapplication'sownroutes.ThefollowinglinesinitializeExpress,tellingitwheretolookforviewtemplatesandwhatrenderingenginetouse(inourcase,Hogan):

varapp=express();

//viewenginesetup

app.set('views',path.join(__dirname,'views'));

app.set('viewengine','{views}');

Therestofthefileconsistsofcallstoapp.use().Theseregistervariousdifferentmiddlewareforprocessingtherequest.Theorderinwhichtheyareregisteredformsarequestprocessingpipeline.YoumightalreadybefamiliarwiththispatternfromservletfiltersinJava,ortheIAppBuilder/IApplicationBuilder/IBuilderinterfacesinOWINandASP.NET.Don'tworryifnotthough;we'llexploremiddlewarethoroughlyhere.

UnderstandingExpressmiddlewareMiddlewarefunctionsarethefundamentalbuildingblocksofanExpressapplication.Theyaresimplyfunctionsthattakerequestandresponsearguments(justlikeourlistenerfunctionsbefore)andareferencetothenextmiddlewareinthechain.

Eachmiddlewarefunctioncanmanipulatetherequestandresponseobjectsbeforepassingontothenextmiddlewareinthechain.Bychainingmiddlewaretogetherinthisway,youcanbuildcomplexfunctionalityfromsimplemodularcomponents.Italsoallowscleanseparationbetweenyourapplicationlogicandcross-cuttingconcernssuchaslogging,authentication,orerrorhandling.

Insteadofpassingcontroltothenextmiddlewareinthechain,afunctioncanalsoendtheprocessingoftherequestandreturnaresponse.Middlewarecanalsobemountedtospecificpathsorrouterinstances,forexample,ifwewantenhancedloggingonaparticularpartofoursite.

Infact,Expressroutesarejustanotherexampleofmiddleware:theroutesthatwehavealreadylookedatareordinarymiddlewarefunctionswiththesamethreeargumentsnotedabove.Theyjusthappentobemountedtoaspecificpathandtoreturnaresponse.

Implementingerrorhandling

Let'stakeacloserlookatsomeofthemiddlewareinapp.js.First,lookatthe404errorhandler:

app.use(function(req,res,next){

varerr=newError('NotFound');

err.status=404;

next(err);

});

Thisfunctionalwaysreturnsaresponse.Sowhydowenotalwaysgeta404fromourapplication?Rememberthatmiddlewareiscalledinorder,andtheroutes(whichareregisteredbeforethisfunction)returnaresponseanddon'tcallthenextmiddleware.Thismeansthatthe404functionwillonlybecalledforrequeststhatdon'tmatchanyroute,whichisexactlywhatwewant.

Whatabouttheothertwoerrorhandlersinapp.js?Theyreturna500responsewithacustomerrorpage.Whydoesourapplicationnotreturna500responseinallcases?Howdothesegetexecutedifanothermiddlewarethrowsanerrorbeforecallingnext()?

Error-handlingisaspecialcaseinExpress.Error-handlingmiddlewarefunctionstakefourargumentsinsteadofthree,withthefirstparameterbeinganerror.Theyshouldberegisteredlast,afterallothermiddlewares.

Inthecaseofanerror(eitheranerrorbeingthrownoramiddlewarefunctionpassinginanerrorargumentwhencallingnext),Expresswillskipanyothernon-errorhandlingmiddlewareand

startexecutingtheerrorhandlers.

UsingExpressmiddleware

Let'sseesomeExpressmiddlewareinactionbymakinguseofcookieparsingmiddleware(whichisalreadypartoftheskeletonapplicationcreatedbyexpress-generator).Wecandothisbyusingacookietostorehowmanytimessomeonehasvisitedthesite.Updateroutes/index.jsasfollows:

router.get('/',function(req,res,next){

varvisits=parseInt(req.cookies.visits)||0;

visits+=1;

res.cookie('visits',visits);

res.render('index',

{title:'Express',name:'World',visits:visits}

);

});

Andaddanewlinetoviews/index.hjs:

<p>Youhavevisitedthissite{{visits}}time(s).</p>

Nowvisithttp://localhost:3000/againandrefreshthepageafewtimes.Youshouldseethevisitcountincreasebasedonthevaluestoredinthecookie.Toseewhatthecookieparsingmiddlewareisdoingforus,trydeletingorcommentingoutthefollowinglinefromapp.jsandreloadingthepage:

app.use(cookieParser());

Asyoucanseefromtheerror,thecookiespropertyoftherequestisnowundefined.ThecookieparsingmiddlewarelooksatthecookieheaderoftherequestandturnsitintoaconvenientJavaScriptobjectforus.Thisisacommonusecaseformiddleware.ThebodyParsermiddlewarefunctionsdoaverysimilarjobwiththerequestbody,turningrawtextintoaJavaScriptobjectthatiseasiertouseinourroutes.

Notethattheerrorresponseabovealsodemonstratesourerrorhandlingmiddleware.Trycommentingouttheerrorhandlersattheendoftheapp.jsfileandreloadingthepageagain.Wenowgetthedefaultstacktraceratherthanthecustomerrorresponsedefinedinourhandler.

SummaryInthischapter,weinstalledNode.js,sawhowtointeractwithitfromthecommandline,andstartedusingittowritewebapplications.WelearnedaboutExpressandhowwecanstructureanapplicationusingroutesandmiddleware.

Althoughwe'veseensomecodeinthischapter,wehaven'treallyexploredtheJavaScriptsyntaxindetail.Beforeaddingmorefunctionalitytoourapplication,weshouldmakesurethatwe'reuptospeedwithJavaScript.Thisisthesubjectofthenextchapter.

Chapter3.AJavaScriptPrimerIt'simportanttohaveasolidunderstandingofJavaScripttowriteNode.jsapplications.JavaScriptisnotalargeorcomplexlanguage,butitmayseemunusual,andhasafewquirksandgotchastowatchoutfor.

TherecentreleaseofECMAScript2015(previouslynamedES6)introducesanumberofnewlanguagefeaturestomakeJavaScriptprogrammingeasierandsafer.NotallES2015featuresareavailableinallimplementationsyet.However,alltheES2015featureswe'llmentioninthischapterareavailableinNode.jsandinmostotherJavaScriptenvironments.

Inthischapter,we'llfamiliarizeourselveswithJavaScriptsowecanwriteNode.jsapplicationswithconfidence.Wewillcoverthefollowingtopics:

TheJavaScripttypesystemJavaScriptasafunctionalprogramminglanguageObject-orientedprogramminginJavaScriptJavaScript'sprototype-basedinheritance

IntroducingJavaScripttypesJavaScriptisadynamically-typedlanguage.Thesemeansthattypesarecheckedatruntimewhenyoutrytodosomethingwithavariable,ratherthanbyacompiler.Forexample,thefollowingisvalidJavaScriptcode:

varmyVariable=0;

console.log(typeofmyVariable);//Prints"number"

myVariable="1";

console.log(typeofmyVariable);//Prints"string"

Althoughvariablesdohaveatype,thismaychangethroughoutthelifetimeofthevariable.

JavaScriptalsotriestoimplicitlyconverttypeswherepossible,forexample,usingtheequalityoperator:

console.log(2=="2");//Prints"true"

AlthoughthismightmakesenseforfrontendJavaScript(forexamplecomparingagainstthevalueofaforminput),ingeneral,itismorelikelytobeasourceoferrorsorconfusion.Forthisreason,itisrecommendedtoalwaysusethestrictequalityandinequalityoperators:

console.log(2==="2");//Prints"false"

console.log(2!=="2");//Prints"true"

JavaScriptprimitivetypesJavaScripthasasmallnumberofprimitivetypes,similartoC#andJava.Thesearestring,number,andBoolean,aswellasthespecialsingle-valuedtypes,nullandundefined.ES2015alsoaddsthesymboltype,butwewon'tcoverithereasitsusecasesaremoreadvanced.

Stringsareimmutable,likeinC#andJava.Concatenatingstringscreatesanewstringinstance.Stringliteralscanbedefinedwithdoublequotes(asinC#orJava)orsinglequotes.Thesecanbeusedinterchangeably(usuallywhateveriseasiertoavoidescaping).

ES2015alsointroducessupportfortemplatestrings,whicharedefinedusingbackticksandcanincludeinterpolatedexpressions.

Hereareseveralwaystodefinethesamestring:

varsingleQuoted='"Hey",Isaid,"I\'mastring"';

vardoubleQuoted="\"Hey\",Isaid,\"I'mastring\"";

console.log(doubleQuoted===singleQuoted);//Prints"true"

varexpression='Hey';

vartemplated=`"${expression}",Isaid,"I'mastring"`;

console.log(templated===singleQuoted);//Prints"true"

NumberisJavaScript'sonlybuilt-innumerictype.Itisadouble-precision64-bitfloating-pointnumber,likedoubleinC#orJava.IthasspecialvaluesNaN(notanumber)andInfinityforvaluesthatcannotberepresentedotherwise:

console.log(1/0);//Prints"Infinity"

console.log(Infinity+1);//Prints"Infinity"

console.log((1/0)===(2/0));//Prints"true"

varnotANumber=parseInt("foo");

console.log(notANumber);//Prints"NaN"

console.log(notANumber===NaN);//Prints"false"

console.log(isNaN(notANumber));//Prints"true"

Note

NotethatalthoughthereisonlyasingleNaNvalue,itisnottreatedasequaltoitself.JavaScriptprovidesthespecialisNaNfunctionfortestingwhetheravariablecontainstheNaNvalue.

Thenulltypehasasingleinstance,representedbytheliteralnull,justasinC#orJava.JavaScriptalsohastheundefinedtype.Variablesorparametersthathaveneverbeenassignedwillhavethevalueundefined:

vardeclared;

console.log(typeofdeclared);//Prints"undefined"

console.log(declared===undefined);//Prints"true"

console.log(typeofundeclared);//Prints"undefined"

console.log(undeclared===undefined);//throwsReferenceError

Notethatourundeclaredidentifiercannotbeaccessedasavariableinnormalcodebecauseithasnotbeendeclared.However,wecanpassittothetypeofoperator,whichevaluatestotheundefinedtype.

Functionalobject-orientedprogrammingJavaScriptisafunctionalobject-orientedprogramminglanguage.However,itisquitedifferenttootherobject-orientedprogramminglanguagessuchasC#orJava.Despitehavingasimilarsyntax,therearesomeimportantdifferences.

FunctionalprogramminginJavaScriptInJavaScript,functionsarefirst-classobjects.Thismeansthatfunctionscanbetreatedlikeanyotherobject:theycanbecreateddynamically,assignedtovariables,orpassedintomethodsasarguments.

Thismakesitveryeasytospecifyeventcallbacks,ortoprograminamorefunctionalstyleusinghigher-orderfunctions.Higher-orderfunctionsarefunctionsthattakeotherfunctionsasarguments,and/orreturnanotherfunction.Here'satrivialexampleoffilteringanarrayofnumbersfirstinanimperativestyleandtheninafunctionalstyle.NotethatthisexamplealsoshowsJavaScript'sarrayliteralnotationforcreatingarrays,usingsquarebrackets.ItalsodemonstratesJavaScript'sconditionalconstructandoneofitsloopconstructs,whichshouldbefamiliarfromotherlanguages:

varnumbers=[1,2,3,4,5,6,7,8];

varfilteredImperatively=[];

for(vari=0;i<numbers.length;++i){

varnumber=numbers[i];

if(number%2===0){

filteredImperatively.push(number);

}

}

console.log(filteredImperatively);//Prints[2,4,6,8]

varfilteredFunctionally=

numbers.filter(function(x){returnx%2===0;});

console.log(filteredFunctionally);//Prints[2,4,6,8]

Thesecondapproachintheexamplemakesuseofafunctionexpressiontodefineanew,anonymousfunctioninline.Ingeneral,thisisreferredtoasalambdaexpression(afterlambdacalculusinmathematics).Thisfunctionispassed-intothebuiltinfilterexpressionavailableonJavaScriptarrays.

InC#,assignmentandpassingofbehaviorwasoriginallyonlypossibleusingdelegates.SinceC#3.0,supportforlambdaexpressionsmakesitmucheasiertousefunctionsinthisway.Thisallowsamorefunctionalstyleofprogramming,forexample,usingC#'sLanguage-IntegratedQuery(LINQ)features.

InJava,foralongtimetherewasnonativewayforafunctiontoexistindependently.Youwouldhavetodefineamethodona(possiblyanonymous)classandpassthisaround,addingalotofboilerplate.Java8introducessupportforlambdaexpressionsinasimilarwaytoC#.

WhileC#andJavamayhavetakenawhiletocatchup,youmightbethinkingthatJavaScriptisnowfallingbehind.ThesyntaxfordefininganewfunctioninJavaScriptisquiteclumsycomparedtothelambdasyntaxinC#andJava.

ThisisespeciallyunfortunatesinceJavaScriptusesaC-likesyntaxforfamiliaritywithother

languageslikeJava!ThisisresolvedinES2015witharrowfunctions,allowingustorewritethepreviousexampleasfollows:

varnumbers=[1,2,3,4,5,6,7,8];

varfilteredFunctionally=numbers.filter(x=>x%2===0);

console.log(filteredFunctionally);//Prints[2,4,6,8]

Thisisasimplearrowfunctionwithasingleargumentandasingleexpression.Inthiscase,theexpressionisimplicitlyreturned.

Note

Itcanbeusefultoreadthe=>notationinarrowfunctionsasgoesto.

Arrowfunctionsmayhavemultiple(orzero)arguments,inwhichcasetheymustbesurroundedbyparentheses.Ifthefunctionbodyisenclosedinbraces,itmaycontainmultiplestatements,inwhichcasethereisnoimplicitreturn.TheseareexactlythesamesyntaxrulesasforlambdaexpressionsinC#.

Hereisamorecomplexarrowfunctionexpressionthatreturnsthemaximumofitstwoarguments:

varmax=(a,b)=>{

if(a>b){

returna;

}else{

returnb;

}

};

UnderstandingscopesinJavaScriptTraditionally,inJavaScript,thereareonlytwopossiblevariablescopes:globalandfunctional.Thatis,anidentifier(avariablename)isdefinedglobally,orforanentirefunction.Thiscanleadtosomesurprisingbehavior,forexample:

functionscopeDemo(){

for(vari=0;i<10;++i){

varj=i*2;

}

console.log(i,j);

}

scopeDemo();

Inmostotherlanguages,youwouldexpectitoexistforthedurationoftheforloop,andjtoexistforeachloopiteration.Youwouldthereforeexpectthisfunctiontologundefinedundefined.Infact,itlogs1018.Thisisbecausethevariablesarenotscopedtotheblockoftheforloop,buttotheentirefunction.Sotheprecedingcodeisequivalenttothefollowing:

functionscopeDemo(){

vari,j;

for(i=0;i<10;++i){

j=i*2;

}

console.log(i,j);

}

scopeDemo();

JavaScripttreatsallvariabledeclarationsasiftheyweremadeatthetopofthefunction.Thisisknownasvariablehoisting.Althoughconsistent,thiscanbeconfusingandleadtosubtlebugs.

ES2015introducestheletkeywordfordeclaringvariables.Thisworksexactlythesameasvarexceptthatvariablesareblock-scoped.Thereisalsotheconstkeyword,whichworksthesameasletexceptthatitdoesnotallowreassignment.Itisrecommendedthatyoualwaysuseletratherthanvar,anduseconstwhereverpossible.Checkthefollowingcodeforexample:

functionscopeDemo(){

"usestrict";

for(leti=0;i<10;++i){

letj=i*2;

}

console.log(i,j);//ThrowsReferenceError:iisnotdefined

}

scopeDemo();

Notethe"usestrict"stringintheprecedingexample.We'lldiscussthisinthenextsection.

Strictmode

The"usestrict"stringisahinttotheJavaScriptinterpretertoenableStrictMode.Thismakesthelanguagesaferbytreatingcertainusagesofthelanguageaserrors.Forexample,

mistypingavariablenamewithoutstrictmodewilldefineanewvariableatthegloballevel,ratherthancausinganerror.

StrictmodeisalsonowusedbysomebrowserstoenablefeaturesinthenewestversionofJavaScript,suchastheletandconstkeywordspreviouslyshown.Ifyouarerunningtheseexamplesinabrowser,youmayfindthattheprecedinglistingdoesn'tworkwithoutstrictmode.

Inanycase,youshouldalwaysenablestrictmodeinallofyourproductioncode.The"usestrict"stringaffectsallcodeinthecurrentscope(thatis,JavaScript'straditionalfunctionalorglobalscope),soshouldusuallybeplacedatthetopofafunction(orthetopofamodule'sscriptfileinNode.js).

Object-orientedprogramminginJavaScriptAnythingthatisnotoneofJavaScript'sbuilt-inprimitives(strings,number,null,andsoon)isanobject.Thisincludesfunctions,aswe'veseenintheprevioussection.Functionsarejustaspecialtypeofobjectthatcanbeinvokedwitharguments.Arraysareaspecialtypeofobjectwithlist-likebehavior.Allobjects(includingthesetwospecialtypes)canhaveproperties,whicharejustnameswithavalue.YoucanthinkofJavaScriptobjectsasadictionarywithstringkeysandobjectvalues.

Objectscanbecreatedwithpropertiesusingtheobjectliteralnotation,asinthefollowingexample:

varmyObject={

myProperty:"myValue",

myMethod:function(){

return`myPropertyhasvalue"${this.myProperty}"`;

}

};

console.log(myObject.myMethod());

Youmightfindthisnotationfamiliarevenifyou'veneverwrittenanyJavaScript,asitisthebasisforJSON.Notethatamethodisjustanobjectpropertythathappenstohaveafunctionasitsvalue.Alsonotethatwithinmethods,wecanrefertothecontainingobjectusingthethiskeyword.

Finally,notethatwedidnotneedtodefineaclassforourobject.JavaScriptisunusualamongstobject-orientedlanguagesinthatitdoesn'treallyhaveclasses.

Programmingwithoutclasses

Inmostobject-orientedlanguages,wecandeclaremethodsinaclassforusebyallofitsobjectinstances.Wecanalsosharebehaviorbetweenclassesthroughinheritance.

Let'ssaywehaveagraphwithaverylargenumberofpoints.Thesemayberepresentedbyobjectsthatarecreateddynamicallyandhavesomecommonbehavior.Wecouldimplementpointslikethis:

functioncreatePoint(x,y){

return{

x:x,

y:y,

isAboveDiagonal:function(){

returnthis.y>this.x;

}

};

}

varmyPoint=createPoint(1,2);

console.log(myPoint.isAboveDiagonal());//Prints"true"

OneproblemwiththisapproachisthattheisAboveDiagonalmethodisredefinedforeachpointonourgraph,thustakingupmorespaceinmemory.

Wecanaddressthisusingprototypalinheritance.AlthoughJavaScriptdoesn'thaveclasses,objectscaninheritfromotherobjects.Eachobjecthasaprototype.Ifwetrytoaccessapropertyonanobjectandthatpropertydoesn'texist,theinterpreterwilllookforapropertywiththesamenameontheobject'sprototypeinstead.Ifitdoesn'texistthere,itwillchecktheprototype'sprototype,andsoon.Theprototypechainwillendwiththebuilt-inObject.prototype.

Wecanimplementthisforourpointobjectsasfollows:

varpointPrototype={

isAboveDiagonal:function(){

returnthis.y>this.x;

}

};

functioncreatePoint(x,y){

varnewPoint=Object.create(pointPrototype);

newPoint.x=x;

newPoint.y=y;

returnnewPoint;

}

varmyPoint=createPoint(1,2);

console.log(myPoint.isAboveDiagonal());//Prints"true"

TheisAboveDiagonalmethodnowonlyexistsonceinmemory,onthepointPrototypeobject.

WhenwetrytocallisAboveDiagonalonanindividualpointobject,itisnotpresent,butitisfoundontheprototypeinstead.

Notethatthistellsussomethingimportantaboutthethiskeyword.Itactuallyreferstotheobjectthatthecurrentfunctionwascalledon,ratherthantheobjectitwasdefinedon.

Creatingobjectswiththenewkeyword

Wecanrewritetheprecedingcodeexampleinaslightlydifferentform,asfollows:

varpointPrototype={

isAboveDiagonal:function(){

returnthis.y>this.x;

}

}

functionPoint(x,y){

this.x=x;

this.y=y;

}

functioncreatePoint(x,y){

varnewPoint=Object.create(pointPrototype);

Point.apply(newPoint,arguments);

returnnewPoint;

}

varmyPoint=createPoint(1,2);

Thismakesuseofthespecialargumentsobject,whichcontainsanarrayoftheargumentstothecurrentfunction.Italsousestheapplymethod(whichisavailableonallfunctions)tocallthePointfunctiononthenewPointobjectwiththesamearguments.

Atthemoment,ourpointPrototypeobjectisn'tparticularlycloselyassociatedwiththePointfunction.Let'sresolvethisbyusingthePointfunction'sprototypepropertyinstead.Thisisabuilt-inpropertyavailableonallfunctionsbydefault.Itjustcontainsanemptyobjecttowhichwecanaddadditionalproperties:

functionPoint(x,y){

this.x=x;

this.y=y;

}

Point.prototype.isAboveDiagonal=function(){

returnthis.y>this.x;

}

functioncreatePoint(x,y){

varnewPoint=Object.create(Point.prototype);

Point.apply(newPoint,arguments);

returnnewPoint;

}

varmyPoint=createPoint(1,2);

Thismightseemlikeaneedlesslycomplicatedwayofdoingthings.However,JavaScripthasaspecialoperatorthatallowsustogreatlysimplifythepreviouscode,asfollows:

functionPoint(x,y){

this.x=x;

this.y=y;

}

Point.prototype.isAboveDiagonal=function(){

returnthis.y>this.x;

}

varmyPoint=newPoint(1,2);

ThebehaviorofthenewoperatorisidenticaltoourcreatePointfunctioninthepreviousexample.Thereisonesmallexception:ifthePointfunctionactuallyreturnedavalue,thenthiswouldbeusedinsteadofnewPoint.ItisconventionalinJavaScripttostartfunctionswithacapitalletteriftheyareintendedtobeusedwiththenewoperator.

Programmingwithclasses

AlthoughJavaScriptdoesn'treallyhaveclasses,ES2015introducesanewclasskeyword.Thismakesitpossibletoimplementsharedbehaviorandinheritanceinawaythatmaybemorefamiliarcomparedtootherobject-orientedlanguages.

Theequivalentofourprecedingcodewouldlooklikethefollowing:

classPoint{

constructor(x,y){

this.x=x;

this.y=y;

}

isAboveDiagonal(){

returnthis.y>this.x;

}

}

varmyPoint=newPoint(1,2);

Notethatthisreallyisequivalenttoourprecedingcode.Theclasskeywordisjustsyntacticsugarforsettinguptheprototype-basedinheritancealreadydiscussed.

Class-basedinheritance

Asmentionedbefore,anobject'sprototypemayinturnhaveanotherprototype,allowingachainofinheritance.Settingupsuchachainbecomesquitecomplicatedusingtheprototype-basedapproachfromtheprevioussection.Itismuchmoreintuitiveusingtheclasskeyword,asinthefollowingexample(whichmightbeusedforplottingagraphwitherrorbars):

classUncertainPointextendsPoint{

constructor(x,y,uncertainty){

super(x,y);

this.uncertainty=uncertainty;

}

upperLimit(){

returnthis.y+this.uncertainty;

}

lowerLimit(){

returnthis.y-this.uncertainty;

}

}

varmyUncertainPoint=newPoint(1,2,0.5);

SummaryInthischapter,wehaveintroducedJavaScript'stypesystem,understoodfunctionsasfirst-classobjectsinJavaScript,seenhowJavaScriptdiffersfromotherobject-orientedlanguages,implementedinheritanceusingprototypesandclasses,andlearnedthenewfeaturesofECMAScript2015(ES6)thatmakethelanguagesaferandmoreintuitivetouse.

NowthatyouhaveafirmgroundinginJavaScript,youcanstartwritingNode.jsapplicationswithconfidence.Inthenextchapter,wewillexpandonourExpressprojectandseehowthemodulesysteminNode.jsallowsustostructureourcodebase.

Chapter4.IntroducingNode.jsModulesNowthatwe'reuptospeedwiththesyntaxoftheJavaScriptlanguage,wecanstartbuildingupourapplication.Todothis,weneedtoknowhowtostructureourapplicationtoallowittogrowinamaintainableway.

Inthischapter,wewillcoverthefollowingtopics:

StructuringJavaScriptcodewithmodulesDeclaringandusingourownmodulesOrganizingmodulesintofilesanddirectoriesImplementinganExpressmiddlewaremodule

OrganizingyourcodebaseMostprogrammingplatformsprovideseveralmechanismsforstructuringyourcode.ConsiderC#/.NETorJava:youcanuseclasses,namespacesorpackages,andcompilationunits(assembliesorJAR/WARfiles).Noticetherangefromsmall-scaleorganizationalunits(classes)tolarge-scaleones(assemblies).Thisallowsyoutomakeacodebasemoreapproachablebyprovidingorderateachlevelofdetail.

Classicbrowser-basedJavaScriptdevelopmentwasquiteunstructured.Functionsweretheonlybuilt-inlanguagefeaturefororganizingyourcode.Youcouldsplityourcodeintoseparatescriptfiles,buttheseallsharethesameglobalcontextwithinawebpage.

Overtime,peoplehavedevelopedwaysoforganizingJavaScriptcode.Thestandardapproachnowistousemodules.ThereareafewdifferentmodulesystemsavailableforJavaScript,buttheyallworkinasimilarway.Eachmodulesystemincludesthefollowingaspects:

AwayofdeclaringamodulewithanameanditsownscopeAwayofdefiningfunctionalityprovidedbythemoduleAwayofimportingamoduleintoanotherscript

Ineachsystem,whenyouimportamodule,yougetaplainJavaScriptobjectthatyoucanassigntoavariable.Formostmodules,thiswillbeanobjectwithseveralpropertiescontainingfunctions.ButitcouldbeanyvalidJavaScriptobject,forexample,asinglefunction.

Mostmodulesystemsexpectoratleastencourageyoutodefineeachmoduleinaseparatefile,justasyouwouldwithclassesinotherlanguages.Itisalsocommonforlargemodulestobecomposedofother,smaller,modules.Thesewouldbegroupedtogetherunderthesamedirectory.Inthisway,modulesactmorelikenamespacesorpackages.

Theflexibilityofmodulesmeansthatyoucanusethemtostructureyourcodeatdifferentscales.Thelackofabuilt-inhierarchyoforganizationalunitsinJavaScriptprovidesmoreflexibility.Italsoforcesyoutothinkmoreabouthowyoustructureyourcode.

JavaScriptmodulesystemsECMAScript2015introducesmodulesasabuilt-infeatureofthelanguage.Theyhavebeencommonpracticeforawhile,though.Forclient-sideprogramming,thispracticehasreliedonusingthird-partylibrariestoprovideamodulesystem.

YoumayhaveseenRequireJS,whichprovidesawayofusingfunctionstodefinemodules.RequireJSusesplainJavaScriptandworksinanyenvironment.Itismostusefulinthebrowser,whereadditionalmodulesmaybeloadedovertheInternet.RequireJSaddressessomeofthepitfallsofloadingadditionalscriptsdynamicallyandasynchronously.

TheNode.jsenvironmenthasitsownmodulesystem,whichwewilllookatintherestofthischapter.Itmakesuseofthefilesystemfororganizingmodules.

Tip

YoumightcomeacrossthetermsAMDorCommonJS.Thesearestandardsfordefiningmodules.RequireJSisanimplementationofAMD,andNode.jsmodulesfollowtheCommonJSstandard.ECMAScript2015modulesdefineanewstandardwithnewexportandimportlanguagekeywords.Thesyntaxisquitesimilar,though,totheNode.jsmodulesystemwe'llbeusinginthisbook,anditiseasytoswitchbetweenthetwo.

CreatingmodulesinNode.jsWe'veactuallyalreadyusedseveralNode.jsmodulesandcreatedsomeofourown.Let'slookagainatourapplicationfromChapter2,GettingStartedwithNode.js.

Thefollowingcodeisfromroutes/index.jsandroutes/users.js:

module.exports=router;

Thefollowingisthecodefromapp.js:

varexpress=require('express');

varpath=require('path');

varfavicon=require('serve-favicon');

varlogger=require('morgan');

varcookieParser=require('cookie-parser');

varbodyParser=require('body-parser');

varroutes=require('./routes/index');

varusers=require('./routes/users');

Eachofourroutes(indexandusers)isamodule.Theyexposetheirfunctionalityusingthebuilt-inmoduleobject,whichisdefinedbyNode.jsasavariablescopedtoeachmodule.Intheprecedingexample,theobjectprovidedbyeachofourroutemodulesisanExpressrouterinstance.Theapp.jsscriptimportsthesemodulesusingthebuilt-inrequirefunction.

Observethatapp.jsalsoimportsvariousnpmpackagesusingrequire.Notethatitusesfilepathstoreferenceourownmodules,whereasnpmmodulesarereferencedbyname.

Let'slookathowNode.jsmodulessatisfythethreeaspectsofJavaScriptmodulefunctionality.

DeclaringamodulewithanameanditsownscopeInNode.js,eachseparateJavaScriptfileisautomaticallytreatedasanewmodule.Unlikescriptsloadedintoawebpage,eachfilehasitsownscope.Thenameofthemoduleisthenameofthefile.

DefiningfunctionalityprovidedbythemoduleNode.jsprovidestwobuilt-invariablesforexportingfunctionalityfromamodule.Thesearemodule.exportsandexports.module.exportsisinitializedtoanemptyobject.exportsisjustareferencetomodule.exports.Itisequivalenttothefollowingappearingbeforeyourscript:

varexports=module.exports={};

Whateveriscontainedinthemodule.exportsvariableattheendofyourscriptistheexportedvalueofyourmodule.Thiswillbereturnedwheneveryourmoduleisimportedelsewhere.Thefollowingareallequivalent:

module.exports.foo=1;

module.exports.bar=2;

module.exports={foo:1,bar:2};

exports.foo=1;

exports.bar=2;

Notethatthefollowingisnotthesameasthepreviousexamples.Itjustreassignsexports,butdoesn'taltermodule.exportsatall:

exports={foo:1,bar:2};

ImportingamoduleintoanotherscriptNode.jsprovidesanotherbuilt-invariableforimportingmodules.Thisistherequirefunctionwesawinapp.jsearlierinthechapter.ThisfunctionisprovidedbyNode.jsandalwaysavailable.Ittakesasingleargument,whichisthenameorpathofthemoduleyouwanttoimport.Thefollowingexcerptsfromapp.jsdemonstrateloadingathird-partymodulebynameandoneofourownmodulesbyafilepath:

varexpress=require('express');

...

varroutes=require('./routes/index');

Notethatwedon'tneedtospecifythe.jsfileextensionforourownmodule.Node.jswillautomaticallyaddthisforus.

Definingadirectory-levelmoduleAsmentionedatthebeginningofthischapter,modulescanalsoactmorelikenamespaces.Wecantreatawholedirectoryasamodule,consistingofsmallermodulesinindividualfiles.Thesimplestwaytodothisistocreateanindex.jsfileinthedirectory.

Whencallingrequire('./directoryName'),Node.jswillattempttoloadafilenamed'./directoryName/index.js'(relativetothecurrentscript).Thereisnothingspecialaboutindex.jsitself.Thisisjustanotherscriptfilethatexposesanentrypointtothemodule.IfdirectoryNamecontainsapackage.jsonfile,Node.jswillloadthisfilefirstandseeifitspecifiesamainscript,inwhichcaseNode.jswillloadthisscriptinsteadoflookingforindex.js.

Toimportlocalmodules,weuseafileordirectorypath,thatis,somethingstartingwith'/','../',or'./'asintheprecedingexample.Ifwecallrequirewithaplainstring,Node.jstreatsitasrelativetothenode_modulesfolder.Thenpmpackagesarejustdirectory-levelmodulesunderthisfolder.Wewilllookatdefiningourownnpmpackagesinmoredetailinalaterchapter.

ImplementinganExpressmiddlewaremoduleLet'sreturntotheNode.jsapplicationwestartedinChapter2,GettingStartedwithNode.js.We'regoingtowriteanapplicationwhereuserscansetpuzzlesforoneanother.Firstofall,we'llneedawayofidentifyingthecurrentuser.We'llneedtodothisonmostrequests,makingitacross-cuttingconcern.Thisisagoodusecaseformiddleware.

Fornow,wewillimplementusersinthesimplestwaypossible,juststoringanIDinacookie.Wewilllookintomorerobustidentificationinalaterchapter.Note,however,thatouruseofmiddlewaremeansitwillbeeasytoalterourapproachlateron.Thisconcernisencapsulatedinourusermiddleware,soweonlyneedtochangeitinoneplace.

First,weneedawayofgeneratinguniqueIDs.Forthis,wewillusetheUUIDmodulefromnpm.Wecanaddthistoourprojectbyrunningthefollowingonthecommandline:

>npminstalluuid--save

The--saveflagstoresthenameofthismoduleinourpackage.jsonfilesothatitwillbeinstalledautomaticallybynpminstall.Thisisusefulforrestoringourapplicationfromacleancheckoutofthesourcecode(recallthatpeoplecommonlyexcludethenode_modulesdirectoryfromsourcecontrol,preciselybecauseitcaneasilyberestoredinthisway).

Nowwearereadytocreateourmiddleware,whichwillplaceundermiddleware/users.js:

'usestrict';

constuuid=require('uuid');

module.exports=function(req,res,next){

letuserId=req.cookies.userId;

if(!userId){

userId=uuid.v4();

res.cookie('userId',userId);

}

req.user={

id:userId

};

next();

};

NoticethatweusetheES2015constkeywordfortheuuidmodulebecausethisreferenceneverchanges.ButweusetheletkeywordfortheuserIdvariablebecausethiscanbereassigned.Alsonoticethatwecallnext()ratherthanreturningaresponse,sothenextmiddlewarecancontinueprocessingtherequest.

Finally,weneedtoaddthismiddlewaretoourapplicationinapp.js:

varusers=require('./middleware/users');

varroutes=require('./routes/index');

varapp=express();

...

app.use(users);

app.use('/',routes);

...

Notethatthisreplacestheimportandusageofthe./routes/usersmodulethatwasgeneratedforus.Thisroutewasn'tparticularlyuseful,butwewilladdmoreroutessoon.

Wecancheckthatourmiddlewareworksbyalteringourindexrouteandviewasfollows:

routes/index.jsrouter.get('/',function(req,res,next){

res.render('index',{title:'Welcome',userId:req.user.id});

});

Thefollowingisthecodeviews/index.hjs:

<body>

<h1>{{title}}</h1>

<p>YouruserIDis{{userId}}.</p>

</body>

Launchtheapplicationandvisithttp://localhost:3000/.Youshouldseearandomly-generateduserID.RefreshthepageandyoushouldretainthesameID.Openthesiteinadifferentbrowser(oranincognito/privatebrowsingwindow).ThisseparatebrowsersessionshouldseeadifferentID.

SummaryInthischapter,wehaveseenhowtouseNode.jsmodulestostructureourcodebase,andhowtocreateanExpressmiddlewaremoduletoimplementcross-cuttingconcerns.

Nowthatwehaveawayofstructuringourcodebaseandameansofidentifyingusers,wecangetonwithimplementingourapplication'sfunctionality.Inthenextchapter,we'llstartaddingsomeinteractivitytoourapplication.

Chapter5.CreatingDynamicWebsitesNowthatwehaveestablishedabasicstructureforourapplication,wecanstarttoaddmorefunctionalityandbuildadynamicwebsitethatrespondstouserinput.

Inthischapter,wewillcoverthefollowingtopics:

AddinganewmoduletoourapplicationforstoringanddeletingdataExposingaJSONAPItohandleuser-submitteddataImplementingcommunicationbetweentheclientandserverusingAjaxBuildingupmorecomplexHTMLviewsusingpartialtemplates

Handlinguser-submitteddataWe'regoingtoimplementtheclassicguessinggameofHangman(seehttps://en.wikipedia.org/wiki/Hangman_(game)).Userswillbeabletopostnewwordstoguess,andtoguesswordspostedbyothers.We'lllookatcreatingnewgamesfirst.

First,we'lladdanewmoduleformanagingourgames.Fornow,we'lljuststoreourgamesinthememory.Ifwewanttoputgamesinsomepersistentstorageinfuture,thisisthemodulewewillchange.Theinterface(thatis,thefunctionsaddedtomodule.exports)canremainthesamethough.

Weaddthefollowingcodeunderservices/games.js:

'usestrict';

constgames=[];

letnextId=1;

classGame{

constructor(id,setBy,word){

this.id=id;

this.setBy=setBy;

this.word=word.toUpperCase();

}

}

module.exports.create=(userId,word)=>{

constnewGame=newGame(nextId++,userId,word);

games.push(newGame);

returnnewGame;

}

module.exports.get=

(id)=>games.find(game=>game.id===parseInt(id,10));

Nowlet'sgothroughourapplicationfromthetopdown.Inourindexview(views/index.hjs),we'lladdsimpleaHTMLformforcreatinganewgame.

<body>

<h1>{{title}}</h1>

<formaction="/games"method="POST">

<inputtype="text"name="word"

placeholder="Enterawordtoguess..."/>

<inputtype="submit"/>

</form>

<body>

Whensubmitted,thisformwillmakeaPOSTrequestto/games.Atthemoment,thiswouldreturna404errorsincewehavenothingmountedatthatroute(youcantrythisinabrowseritifyoulike).Wecanaddanewgamesroutetohandlethisrequest.Weaddthefollowingcodeunderroutes/games.js:

'usestrict';

constexpress=require('express');

constrouter=express.Router();

constservice=require('../services/games');

router.post('/',function(req,res,next){

constword=req.body.word;

if(word&&/^[A-Za-z]{3,}$/.test(word)){

service.create(req.user.id,word);

res.redirect('/');

}else{

res.status(400).send('Wordmustbeatleastthreecharacterslongand

containonlyletters');

}

});

module.exports=router;

Thereisquitealotgoingoninournewroutingmiddleware:

router.postcreatesahandlerforanHTTPPOSTrequest.req.bodycontainsformvalues,thankstothebodyParsermiddlewareinapp.js.req.user.idcontainsthecurrentuser,thankstoourusersmiddleware.res.redirect()issuesaredirecttoreloadthepage.ItisimportanttoalwaysissuearedirectafterasuccessfulPOSTrequest.Thisavoidsduplicateformsubmissions.res.status()setsanalternativeHTTPstatuscodefortheresponse,inthiscasea400foravalidationfailure.

Ourroutelooksforafieldnamedwordintherequestbody.Itthenchecksthisfieldisdefinedandnotempty(bothundefinedandtheemptystringarefalseyinJavaScript,sotheybehaveasfalseinconditionaltests).Italsochecksthatthefieldmatchesaregularexpressionspecifyingourvalidityrule.

Finally,theroutemakesuseofourservicemoduletoactuallycreatethenewgame.Itiscommonpracticeforroutingmiddlewaretodelegateapplicationlogictoothermodules.ItsmainresponsibilityistodefinetheHTTPinterfaceoftheapplication.Othermodulesareresponsibleforimplementingtheactualapplicationlogic.Inthisway,ourroutesandmiddlewarearecomparabletocontrollersinMVCframeworks.

Wealsoneedtomountthisrouteatthe/gamespath.Thefollowingcodeisfromapp.js:

varroutes=require('./routes/index');

vargames=require('./routes/games');

...

app.use('/',routes);

app.use('/games',games);

CommunicatingviaAjaxHavingcreatedagame,weneedawayofplayingit.Sincethewholepointofaguessinggameisthatthewordissecret,wedon'twanttosendthewholewordtotheclient.Instead,wejustwanttoletclientsknowthelengthofthewordandprovideawayforthemtoverifytheirguesses.

Todothis,we'llfirstneedtoexpandourgamesservicemodule:

classGame{

constructor(id,setBy,word){

this.id=id;

this.setBy=setBy;

this.word=word.toUpperCase();

}

positionsOf(character){

letpositions=[];

for(letiinthis.word){

if(this.word[i]===character.toUpperCase()){

positions.push(i);

}

}

returnpositions;

}

}

Nowwecanaddtwonewroutestoourgamesroute:

constcheckGameExists=function(id,res,callback){

constgame=service.get(id);

if(game){

callback(game);

}else{

res.status(404).send('Non-existentgameID');

}

}

router.get('/:id',function(req,res,next){

checkGameExists(

req.params.id,

res,

game=>res.render('game',{

length:game.word.length,

id:game.id

}));

});

router.post('/:id/guesses',function(req,res,next){

checkGameExists(

req.params.id,

res,

game=>{

res.send({

positions:game.positionsOf(req.body.letter)

});

}

);

});

Thesetworoutesmakeuseofasharedfunctionforretrievingthegameandreturninga404statuscodeifitdoesnotexist.TheGEThandlerrendersaview,aswithourindexroute.ThePOSThandlercallsres.send(),passinginaJavaScriptobject.ExpresswillautomaticallyturnthisintoaJSONresponsetotheclient.ThismakesitveryeasytobuildJSON-basedAPIsinexpress.

We'llnowcreateaviewandclient-sidescriptforcommunicatingwiththisAPI.Weaddthefollowingcodeunderviews/game.hjs:

<!DOCTYPEhtml>

<html>

<head>

<title>Hangman-Game#{{id}}</title>

<linkrel="stylesheet"href="/stylesheets/style.css"/>

<script

src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.3/jquery.min.js">

</script>

<scriptsrc="/scripts/game.js"></script>

<basehref="/games/{{id}}/">

</head>

<body>

<h1>Hangman-Game#{{id}}</h1>

<h2id="word"data-length="{{length}}"></h2>

<p>Pressletterkeystoguess</p>

<h3>Missedletters:</h3>

<pid="missedLetters"></p>

</body>

</html>

Weaddthefollowingcodeunderpublic/scripts/game.js:

$(function(){

'usestrict';

varword=$('#word');

varlength=word.data('length');

//Createplaceholdersforeachletter

for(vari=0;i<length;++i){

word.append('<span>_</span>');

}

varguessedLetters=[];

varguessLetter=function(letter){

$.post('guesses',{letter:letter})

.done(function(data){

if(data.positions.length){

data.positions.forEach(function(position){

word.find('span').eq(position).text(letter);

});

}else{

$('#missedLetters')

.append('<span>'+letter+'</span>');

}

});

}

$(document).keydown(function(event){

//Letterkeyshavekeycodesintherange65-90

if(event.which>=65&&event.which<=90){

varletter=String.fromCharCode(event.which);

if(guessedLetters.indexOf(letter)===-1){

guessedLetters.push(letter);

guessLetter(letter);

}

}

});

});

Notethatintheclient-sidescriptwedropbacktotheECMAScript5standard(forexample,varinsteadoflet,andnoarrowfunction).Thisensuresthewidestpossiblecompatibility.ThelatestversionsofallmainstreambrowserswouldsupporttheelementsofES2015syntaxthatwe'vebeenusingsofarthough.

Alsonotethatwedon'thaveNode.jsmodulesavailableontheclientside.Wefallbacktowrappingourcodeinafunctiontoisolatethescope.We'lllookatwaystomakeclient-sidecodemoremodularinalaterchapter.

Ourclient-sidescriptusesjQuery.Wewon'tgointodetailonclient-sideframeworks,butit'sworthquicklyexplainingthefeaturesusedhere.ThejQuerylibraryprovidesaconsistentAPIforDOMmanipulationthatworksacrossallbrowsers,aswellasanumberofusefultoolsforclient-sidefunctionality.

ThemainjQueryAPIisavailablethroughthe$object,whichisafunction.Thefirstthingourscriptdoesiscall$andpassitacallback,whichjQuerywillexecuteoncethepagehasfinishedloading.Ourothercallsto$passinastringoraDOMelement.StringsareinterpretedasaCSSselectorforchoosingelements.Inbothcases,$returnsawrapperaroundasetofDOMelementswithsomeusefulmethods,forexample:

Thedatamethodallowsustoreadtheelements'data-attributesTheappendmethodallowsustoaddnewchildelementsMethodssuchaskeydownallowustobindhandlerfunctionsforevents

Therearealsosomeutilitymethodsdefinedonthe$objectitself.Thesearemorelikestaticmethodsanddon'trelatetoaspecificDOMelement.Thepost()methodisanexampleofthis.

OurscriptusesjQuery'spost()methodtoissueanAjaxPOSTrequest.Thisreturnsanobjectwithadone()method,towhichwecanpassacallbacktobeexecutedwhentherequestcompletes.Here,wecanmakeuseoftheJSONdatareturnedbyourAPI.Inthiscase,wefillin

anypositionsthatmatchourguessedletter.

Ifweruntheapplicationatthispoint,wehavea(very)minimalworkinggame.First,visithttp://localhost:3000/andcreateanewgamebysubmittingavalidword.Thenvisithttp://localhost:3000/games/1toplay.Itshouldlooksomethinglikethefollowing:

ImplementingotherdataoperationsSofar,wehaveseenhowtocreateorretrieveasinglegame,orsubmitasingleguessforagame.Applicationsalsocommonlyneedtolistdataordeleteentries.Theprinciplesherearemuchthesameaswe'veseenalready.Buttoimplementtheseoperations,we'llneedsomenewsyntax.

ListingdatainviewsLet'sfirstallowuserstoseealistofgamesthey'vecreatedorthathavebeencreatedbyothers.Ourchosenviewengine,Hogan,isbasedonMustache,whichhasaverysimplesyntaxfordisplayinglists.Wecanaddthesetwoliststoourindex.hjsview,asfollows:

<h2>Gamescreatedbyyou</h2>

<ulid="createdGames">

{{#createdGames}}

<li>{{word}}</li>

{{/createdGames}}

</ul>

<h2>Gamesavailabletoplay</h2>

<ulid="availableGames">

{{#availableGames}}

<li><ahref="/games/{{id}}">#{{id}}</a></li>

{{/availableGames}}

</ul>

Inordertopopulatetheselists,we'llneedacoupleofnewmethodsinourgames.jsservicemodule:

module.exports.createdBy=

(userId)=>games.filter(game=>game.setBy===userId);

module.exports.availableTo=

(userId)=>games.filter(game=>game.setBy!==userId);

Finally,we'llneedtoexposethesetoourindexviewfromourroute:

varexpress=require('express');

varrouter=express.Router();

vargames=require('../services/games');

router.get('/',function(req,res,next){

res.render('index',{

title:'Hangman',

userId:req.user.id,

createdGames:games.createdBy(req.user.id),

availableGames:games.availableTo(req.user.id)

});

});

module.exports=router;

Now,ourindexpageshowsgamescreatedbythecurrentuserandprovidesconvenientlinkstogamescreatedbyothers.Youcanexperimentwiththisfunctionalitybyusingtwoseparatebrowsersessionsagaintovisithttp://localhost:3000.Theresultshouldlooksomethinglikethefollowing:

IssuingadeleterequestfromtheclientToallowuserstoremovegamesthattheyhavecreated,we'llfirstneedtoaddamethodtoourGameclass:

classGame{

constructor(id,setBy,word){

this.id=id;

this.setBy=setBy;

this.word=word.toUpperCase();

}

positionsOf(character){

letpositions=[];

for(letiinthis.word){

if(this.word[i]===character.toUpperCase()){

positions.push(i);

}

}

returnpositions;

}

remove(){

games.splice(games.indexOf(this),1);

}

}

Nextwecancreateanewhandlerfordeleterequestsinourgamesroute:

router.delete('/:id',function(req,res,next){

checkGameExists(

req.params.id,

res,

game=>{

if(game.setBy===req.user.id){

game.remove();

res.send();

}else{

res.status(403).send(

'Youdon'thavepermissiontodeletethisgame'

);

}

}

);

});

Finally,wecanmakeuseofthisfromtheclient.Thefollowingcodeisfromviews/index.hjs:

<head>

<title>{{title}}</title>

<linkrel="stylesheet"href="/stylesheets/style.css"/>

<script

src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.3/jquery.min.js">

</script>

<scriptsrc="/scripts/index.js"></script>

</head>

...

{{#createdGames}}

<liclass="game">

{{word}}

<aclass="delete"href="/games/{{id}}">(delete)</a>

</li>

{{/createdGames}}

Weaddthefollowingcodeunderpublic/scripts/index.js:

$(function(){

'usestrict';

$('#createdGames').on('click','.delete',function(){

var$this=$(this);

$.ajax($this.attr('href'),{

method:'delete'

}).done(function(){

$this.closest('.game').remove();

});

event.preventDefault();

});

});

Notethat,unlikeGETandPOST,jQueryhasnoconveniencefunctionfordeleterequests.Sowedropbacktothelowerlevel.ajax()functionandspecifytheHTTPmethodexplicitly.

Ifyouvisittheapplicationinabrowserandcreateanewgameagain,youshouldnowseealinktodeletethegame.

SplittingupExpressviewsusingpartialsDeletingagamedoesnotcausethepagetorefresh,butcreatinganewgamedoes.WecanfixthisbycreatinggamesviaanAjaxcall,consistentwithhowwedeletegames.Inorderforthistowork,theclient-sidescriptthathandlesthecallneedstoknowwhichHTMLtoaddtothepagewhenanewgameiscreated.

WecouldrepeattheHTMLstructureoftheviewwithintheclient-sideJavaScript.However,itwouldbebetterfortheservertoreturnthecorrectHTMLfragment,andtoreusethesametemplateforthisasitusesittorenderthelistonthepageinitially.

WecandothisbysplittingtheHTMLstructureforagamewithinthelistintoapartialview.ThisisaviewtemplateforanHTMLfragmentratherthanacompletepage.Weaddthefollowingcodeunderviews/createdGame.hjs:

<liclass="game">

{{word}}

<aclass="delete"href="/games/{{id}}">(delete)</a>

</li>

Withtheviewenginethatwe'reusing(Hogan),weneedtoletviewsknowaboutavailablepartialswhenrenderingthem(otherviewenginesallowpartialstoberesolvedautomatically).Thefollowingcodeisfromroutes/index.js:

res.render('index',{

title:'Hangman',

userId:req.user.id,

createdGames:games.createdBy(req.user.id),

availableGames:games.availableTo(req.user.id),

partials:{createdGame:'createdGame'}

});

Wecanusethepartialwithinourmainviewasfollows.We'llalsoaddIDstoourHTMLelements,whichwewillreferencefromourclient-sideJavaScriptshortly.Thefollowingcodeisfromviews/index.hjs:

<formaction="/games"method="POST"id="createGame">

<inputtype="text"name="word"id="word"

placeholder="Enterawordtoguess..."/>

<inputtype="submit"/></form>

<h2>Gamescreatedbyyou</h2>

<ulid="createdGames">

{{#createdGames}}

{{>createdGame}}

{{/createdGames}}

</ul>

Nowwecanupdateourgamesroutetoreturnonlythisfragmenttotheclientwhencreatinganewgame.Thefollowingcodeisfromroutes/games.js:

router.post('/',function(req,res,next){

letword=req.body.word;

if(word&&/^[A-Za-z]{3,}$/.test(word)){

constgame=service.create(req.user.id,word);

res.redirect(`/games/${game.id}/created`);

}else{

...

}

});

...

router.get('/:id/created',function(req,res,next){

checkGameExists(

req.params.id,

res,

game=>res.render('createdGame',game));

});

Finally,wecanmakeuseofthisinourclient-sidescript.Thefollowingcodeisfrompublic/scripts/index.js:

$(function(){

'usestrict';

$('#createGame').submit(function(event){

$.post($(this).attr('action'),{word:$('#word').val()},

function(result){

$('#createdGames').append(result);

});

event.preventDefault();

});

...

});

SummaryInthischapter,wehavestartedbuildingoutourownapplicationbycreatingnewmiddlewareandservicemodules.We'vereaduser-submitteddatafromformsandactedonit.We'veimplementedaJSONAPIontheserversideandcommunicatedwiththisfromtheclientusingAjax.We'veusedpartialviewstorendercommoncomponents.

Sofar,we'veseenhowtowriteJavaScriptcodeandimplementvariousfunctionalityinNode.js.Thisisgoodforprototyping,butisn'tenoughforamaintainableproject.It'salsoimportanttowriteautomatedtestsforourcode,whichisthesubjectofthenextchapter.

Chapter6.TestingNode.jsApplicationsSofar,wehaveonlybeentestingourcodebyexercisingitmanually.Thisisn'taverysustainableapproachasourapplicationbecomeslarger.Ideally,weshouldregularlyexerciseallthefunctionalityofourapplicationtocheckforregressions.Thiswouldquicklybecomeprohibitivelytime-consumingifwecontinuedtouseonlymanualtesting.Itismuchmoreeffectivetomaintainasuiteofautomatedtests.Thesealsobringmanyotherbenefits,forexample,actingasdocumentationofourcodeforotherdevelopers.

Inthischapter,wewillcoverthefollowingtopics:

WritingautomatedunittestsforourapplicationIntroducingnewlibrariestohelpuswritemoredescriptivetestsSeeinghowtocreateandusetestdoublesinJavaScriptExercisingourapplication'swebinterfaceusingHTTPclienttestsAddingfull-stackintegrationtestsusingbrowserautomationEstablishingastructureforwritingfurthertestsasweexpandourcodebase

WritingasimpletestinNode.jsNode.jscomeswithabuilt-inmodulecalledassertthatcanbeusedfortesting.WecanuseittowriteasimpletestforthegamesservicethatwewroteinChapter5,BuildingDynamicWebsites.WeaddthefollowingcodeundergameServiceTest.js:

'usestrict';

letassert=require('assert');

letservice=require('./services/games.js')

//Given

service.create('firstUserId','testing');

//When

letgames=service.availableTo('secondUserId');

//Then

assert.equal(games.length,1);

letgame=games[0];

assert.equal(game.setBy,'firstUserId');

assert.equal(game.word,'TESTING');

Notethattheassert.equalfunctiontakestheactualvalueasthefirstargumentandtheexpectedvalueasthesecondargument.ThisistheoppositewayaroundtoJUnit'sbuilt-inAssert.Equals,andtheclassic-styleAssert.AreEqualinNUnit.It'simportanttogettheseparameterstherightwayaroundsothattheyappearcorrectlyinerrormessageswhenanassertionfails.

Tip

Given,When,Then

TheGiven,When,andThencommentsintheprecedingtestarenotspecifictoJavaScriptoranyofthetestframeworkswe'llbeusing,butaregenerallyagoodtoolforstructuringteststokeepthemfocusedandreadable.

Wecannowverifyourcodeusingthefollowingcommand:

>nodegameServiceTest.js

>echo%errorlevel%

Anexitcodeof0indicatesthatourtestcompletedsuccessfullywithoutanyerrors.Althoughwehaven'tbeenfollowingtest-drivendevelopment(writingafailingtestfirstbeforeaddinganynewcode),it'sstillimportanttoseeeachtestfailtoconfirmthatit'stestingsomething.TryalteringtheavailableTofunctioninservices/games.jstoreturnanemptyarray,andrunthetestagain.

Notonlydowenowgetanon-zeroexitcode,butwealsogetanerrorcontainingourassertionfailure.Ourtestoutputstillisn'tparticularcompelling,though.Also,thelackofstructureinourtestscriptwillmakeithardertonavigateasweaddmoretests.Wecanaddressbothofthese

issuesbymakinguseofoneofthetestinglibrariesavailableforJavaScript.

StructuringthecodebasefortestsAswewritemoretestsforourapplication,we'llbenefitfromhavingmorestructuretoourtests.It'scommontohaveatleastonetestfileperproductionmodule.Itwillalsobeusefultohaveawayofrunningallofourtestsandseeingtheoverallresult.

We'regoingtostartaddingtestsunderatestdirectory.Fromthispointoninthebook,we'realsogoingtokeepallofourapplicationcodeunderasrcdirectory.Thiswillmakeiteasiertonavigateourcodebaseandtokeepproductionandtestcodeseparate.

Ifyou'refollowingalongwiththebookatthispoint,youshouldmoveapp.jsandallthefolders(apartfromthebinfolder)underanewsrcdirectory,andupdatethestartupscriptasfollowsinbin/www:

varapp=require('../src/app');

vardebug=require('debug')('hangman:server');

varhttp=require('http');

WritingBDD-styletestswithMochaFromC#orJava,youmaybemostfamiliarwiththexUnit-styleoftestsusedbyNUnit,JUnit,andsoon.Thisstylestructurestestsintoclasses,andturnsmethodnamesintotestnames.Thiscanbeabitrestrictive,andisn'tcommoninJavaScripttesting.JavaScripttestframeworksmakeuseofthelessstructured,andmoredynamic,natureofthelanguagetoallowmoreflexibility.

ThereareseveraldifferentstylesforwritingtestsinJavaScript.Themostcommonistheso-calledbehavior-drivendevelopment(BDD)styleinwhichwedescribethebehaviorofourapplicationinplainEnglish.ThisisthedefaultstyleofthemostpopularJavaScripttestingframeworks.Itisalsocommoninframeworksforotherprogrammingplatforms,mostnotablyRSpecforRuby.

We'llbeusingapopulartestframeworknamedMocha.Let'sfirstaddthistoourapplication:

>npminstallmocha--save-dev

Notethat--save-devaddsMochatoourpackage.jsonfileasadevelopmentdependency.Thisindicatesthatit'snotneededinourproductioncode,andnpmdoesn'tneedtoinstallitinproductionenvironments.We'llalsoupdatethisfiletoletnpmrunourtestsusingMocha,byaddingatestscriptasfollows:

"scripts":{

"start":"node./bin/www",

"test":"nodenode_modules/mocha/bin/mochatest/**/*.js"

},

Thistellsnpmtoexecutescriptsunderthe/test/directoryastestsusingMochawhenwerunnpmtestfromthecommandline.

Note

MochaandJasmine

TherearealargenumberofdifferenttestingframeworksavailableforJavaScript.Themostwell-establishedareJasmineandMocha.Theyhavecomparablefeaturesandbothsupportthesamesyntaxforwritingtests.Theyarebothwell-documented,andswitchingbetweenthetwoiseasy.

Jasminewasoriginallyaimedmoreattestingclient-sideJavaScriptinthebrowser.Mochawasoriginallymorefocusedontestingserver-sideNode.jscode.

Nowadays,bothframeworksarewell-suitedforeitherenvironment.Jasminealsohasmorebatteriesincluded,whichcanmakeitquickertogetstartedwith.Mochadelegatesmorefeaturestootherlibraries,givingtheusermorechoiceabouthowtheyprefertowritetests.

Nowwejustneedtoaddsometests!Mochaprovidesglobalfunctionsnameddescribeanditforstructuringourtests.Thesefunctionseachtaketwoarguments:astringdescribingthebehavior

ofourapplicationandacallbackdefiningthetestsforthatbehavior.ThefollowingcodesnippetshowsourprevioustestrewrittenusingMocha.Weaddthefollowingcodeundertest/services/games.js:

'usestrict';

constassert=require('assert');

constservice=require('../../src/services/games.js');

describe('Gameservice',()=>{

constfirstUserId='user-id-1';

constsecondUserId='user-id-2';

describe('listofavailablegames',()=>{

it('shouldincludegamessetbyotherusers',()=>{

//Given

service.create(firstUserId,'testing');

//When

constgames=service.availableTo(secondUserId);

//Then

assert.equal(games.length,1);

constgame=games[0];

assert.equal(game.setBy,firstUserId);

assert.equal(game.word,'TESTING');

});

});

});

Nowtryrunningtheprevioustestusingnpmtest.Youshouldseeoutputlikethefollowing(theexactappearancewilldependonwhatconsoleyouareusing):

Notehowwegetamuchmoredescriptiveoutputofourtests.Alsonotetheuseofnesteddescribecallbacksinourtesttobuildupadescriptionofourapplication.Thebenefitofthisbecomesclearerasweaddmoretests.Tryaddingthefollowingtestafterthefirsttest:

it('shouldnotincludegamessetbythesameuser',()=>{

//Given

service.create(firstUserId,'first');

service.create(secondUserId,'second');

//When

constgames=service.availableTo(secondUserId);

//Then

assert.equal(games.length,1);

constgame=games[0];

assert.notEqual(game.setBy,secondUserId);

});

Runthetestsagainusingnpmtest.Thistime,wegetatestfailurefromMocha:

ResettingstatebetweentestsOursecondtestfailsbecauseitretrievestwogamesfromtheservice.Butthisisnotbecauseourproductioncodeisfailingtofiltergamescorrectly.Infact,therearetwogamescreatedbythefirstuser.Oneofthesehasbeencarriedoverfromtheprevioustest.

It'simportantforteststobeindependentandisolatedfromeachother.Tothisend,weneedtocleanupanystatebetweentests.Inthiscase,wewanttodeleteallthegameswecreated.Thegamesservicedoesn'tgiveusamethodforclearingallgames.Wecanonlyremoveindividualgamesafterretrievingthem.Thereareafewoptionsavailabletoushere:

Wecouldkeeptrackofallthegameswecreateduringeachtestanddeletethemallattheend.Thismightseemthemostobvioussolution,butit'sabitfragile.Itwouldbeeasytomissasinglegamethatmightcauseconfusingtestfailureslater.Wecouldrewritethegamesservicemoduletoexportafunctionforcreatinganewservice,andinstantiateanewserviceforeachtest.Ingeneral,it'sagoodideatotryandisolatetestsbycreatingfreshobjectsundereachtest.However,thisisonlyusefuliftheobjectdoesn'tstoreanyexternalstate.Wemaywellwanttochangetheimplementationofthegamesservicelater,tostoredataexternallyinapersistentdatastore.Wecouldaddaclearmethodtothegamesservicetowipeoutallitsdata.It'snotwrongtocreatemethodslikethisforthepurposesofsupportingtests.However,it'spreferabletointeractwiththeapplicationviaitsexistingAPIifpossible.

Thegamesservicedoesofferawayofretrievingallcurrentgames.WejustneedtopassinauserIDthatdoesn'tmatchthesetterofanygame.Wecanthengothroughanddeleteallgames.Wewanttodothisbeforeeverytest,whichwecandousingMocha'sbeforeEachhook:

describe('Gameservice',()=>{

constfirstUserId='user-id-1';

constsecondUserId='user-id-2';

beforeEach(()=>{

letgamesCreated=service.availableTo("not-a-user");

gamesCreated.forEach(game=>game.remove());

});

describe('listofavailablegames',()=>{

Ifwere-runourtests,theynowbothpasscorrectly.ThereisalsoanafterEachhookinMocha,whichwecouldhaveusedinstead.Thiswouldhaveworked,butit'ssaferforteststodefendthemselvesbycleaningupfirst,ratherthanrelyingonotherteststocleanupafterthemselves.

UsingChaiforassertionsAnotherwaytomakeourtestsmoredescriptiveishowwewriteourassertions.Althoughthebuilt-inNode.jsassertmodulehasbeenusefulsofar,itisabitlimited.Itonlycontainsasmallnumberofsimplemethodsforbasicassertions.

YoumayhaveexperienceofFluentAssertionsorNUnit'sConstraintmodelin.NET,orAssertJinJava.Comparedtothese,theNode.jsassertmodulemightseemquiteprimitive.

ThereareseveralassertionframeworksavailableforJavaScript.We'llbeusingChai(http://chaijs.com),whichsupportsthreedifferentstylesforwritingassertions.TheassertstylefollowsthetraditionalxUnitassertions,asinJUnit,ortheclassicmodelofNUnit.Theshouldandexpectstylesprovideanaturallanguageinterfaceforbuildingmoredescriptiveassertions.

Anyofthesestylesisaperfectlyvalidchoiceforwritingtestassertions.Theimportantthingistopickastyleforyourcodebaseanduseitconsistently.WewillbeusingChai'sexpectsyntaxthroughoutthisbook.ThisisoneofthemorecommonstylesinJavaScripttesting.TheJasminetestframeworkhasbuilt-inassertionsthatfollowasimilarstyle.

Let'sfirstinstallChaibyrunningthefollowingonthecommandline:

>npminstallchai--save-dev

Thenupdateourteststouseit:

constexpect=require('chai').expect;

constservice=require('../../src/services/games.js');

...

it('shouldincludegamescreatedbyotherusers',()=>{

//Given

service.create(firstUserId,'testing');

//When

constgames=service.availableTo(secondUserId);

//Then

expect(games.length).to.equal(1);

constgame=games[0];

expect(game.setBy).to.equal(firstUserId);

expect(game.word).to.equal('TESTING');

});

it('shouldnotincludegamescreatedbythesameuser',()=>{

//Given

service.create(firstUserId,'first');

service.create(secondUserId,'second');

//When

constgames=service.availableTo(secondUserId);

//Then

expect(games.length).to.equal(1);

letgame=games[0];

expect(game.setBy).not.to.equal(secondUserId);

});

Thechangeisn'tparticularlydramaticatthispointaswe'reonlymakingsimpleassertions.Butthenaturallanguageinterfacewillallowustospecifymoredetailedassertionsinadescriptiveway.

CreatingtestdoublesTherearemoretestswecouldwriteforthegamesservice,butlet'slookatadifferentmodulefornow.Howwouldwegoabouttestingourusersmiddleware?Thefollowingcodeisfrommiddleware/users.js:

module.exports=function(req,res,next){

letuserId=req.cookies.userId;

if(!userId){

userId=uuid.v4();

res.cookie('userId',userId);

}

req.user={

id:userId

};

next();

};

Inordertotestthisclass,wewillneedtopassinargumentsforthereq,res,andnextparameterswithwhichourcodeinteracts.Wedon'thavearealrequest,response,ormiddlewarepipelineavailable,soweneedtocreatesomestand-invaluesinstead.Stand-invaluessuchasthisaregenerallycalledtestdoubles.Ourcodereadsanattributefromtherequestandcallsthecookiemethodontheresponse.Wecancreatetestdoublesfortheseasfollows,inanewtestscriptundertest/middleware/users.js:

'usestrict';

constmiddleware=require('../../middleware/users.js');

constexpect=require('chai').expect;

describe('Usersmiddleware',()=>{

constdefaultUserId='user-id-1';

letrequest,response;

beforeEach(()=>{

request={cookies:{}};

response={cookie:()=>{}};

});

it('iftheuseralreadysignedin,readstheirIDfromacookieand

exposestheuserontherequest',()=>{

//Given

request.cookies.userId=defaultUserId;

//When

middleware(request,response,()=>{});

//Then

expect(request.user).to.exist;

expect(request.user.id).to.equal(defaultUserId);

});

});

Here,wesimplycreateaplainJavaScriptobjecttorepresenttherequest.Thisallowsustoverifythattheproductioncodereadsfrom,andwritesto,therequestpropertiescorrectly.Wejustpassintheminimumpossibleinputfortheresponseobjectandthenextfunctiontoallowthecodetoexecute.ThisisveryeasytodoinJavaScript,partlybecauseitisnotstaticallytyped.CreatingtestdoubleslikethisinC#orJavacanbealotmoreworkasthecompilerwillinsistonthetestdoublesmatchingthecorrespondingparametertypes.

Wealsoneedtotestthatourmiddlewarecallsthenextmiddlewareinthechain,asthisisimportantbehavior.Thisisslightlymorecomplexthanjustcreatinganobjectwithsimpleproperties.Wecanstillcreateasuitabletestdoublebydefininganewfunctionthatrecordswhenitiscalled(thiskindoftestdoubleiscalledaspy):

it('callsthenextmiddlewareinthechain',()=>{

//Given

letcalledNext=false;

constnext=()=>calledNext=true;

//When

middleware(request,response,next);

//Then

expect(calledNext).to.be.true;

});

Thisworksperfectlywell,butwillbecomemorecumbersomeifwewanttotestmorecomplexcalls,forexample,ifwewanttocheckformultiplecallsormakefurtherassertionsabouttheargumentspassedin.Wecansimplifythisbymakinguseofaframeworktocreatetestdoublesforus.

CreatingtestdoublesusingSinon.JSSinon.JSisaframeworkforcreatingallkindsoftestdoubles.Let'sfirstinstallitintoourapplicationbyrunningthefollowingonthecommandline:

>npminstallsinon--save-dev

Nowlet'ssimplifyourprevioustestandwriteamorecomplextestusingtestdoublescreatedbySinon.JS:

constexpect=require('chai').expect;

constsinon=require('sinon');

...

it('callsthenextmiddlewareinthechain',()=>{

//Given

constnext=sinon.spy();

//When

middleware(request,{},next);

//Then

expect(next.called).to.be.true;

});

it('iftheuserisnotalreadysignedin,'+

'createsanewuseridandstoresitinacookie',()=>{

//Given

request.cookies.userId=undefined;

response={cookie:sinon.spy()};

//When

middleware(request,response,()=>{});

//Then

expect(request.user).to.exist;

constnewUserId=request.user.id;

expect(newUserId).to.exist;

expect(response.cookie.calledWith(

'userId',newUserId)).to.be.true;

});

Sinon.JSspieskeeptrackofthedetailsofallcallsmadetothemandprovideaconvenientAPIforcheckingthese.Thisallowsustokeepourtestcodesimpleandreadable.TherearemanymorepropertiesthanjustthecalledandcalledWithuserhere.TakealookattheSinon.JSdocumentationathttp://sinonjs.org/docs/#spies-apitoseesomeoftheotherwayswecanverifythecallsmadeagainstaspy.

Note

Spies,stubs,andmocks

IfyoureadmoreoftheSinon.JSdocumentation,you'llseethatit'sveryexplicitaboutthedifferencebetweenspies,stubs,andmocks.ThisisincontrasttomostpopulartestdoubleframeworksinJavaand.NET,whichtendtocallalltestdoublesbythesamename(typicallymockorfake).Inrealitythough,mostinstancesoftestdoublestypicallyonlyactasaspy(usedforverifyingside-effects)orastub(usedforprovidingdata,orthrowingexceptionstotesterror-handling).Atruemockverifiesaspecificsequenceofcallsandreturnsspecificdatatothecodeundertest.AlthoughsomeoftheearlymockingframeworksinJavaand.NETonlysupportedthistypeoftestdouble(nowsometimescalledastrictmock),itisn'tcommonpracticeanymore.Thisisbecauseitquitetightlycouplestestandproductioncodeandmakesrefactoringmoredifficult.It'sespeciallyraretohavemorethanonemock(asopposedtojustastuborspy)inasingletest.

TestinganExpressapplicationWhileusingSinon.JSmakesourtestsneater,theystilldependonthedetailsoftheExpressmiddlewareAPIandhowwe'reusingit.Thismightbeappropriateforourmiddlewaremoduleaswewanttoensurethatitfulfillsaparticularcontract(especiallycallingnextandsettingrequest.user).Formostmiddleware,though,especiallyourroutes,thisapproachwouldcoupleourteststoocloselytoourimplementation.

ItwouldbebettertotesttheactualbehaviorofeachroutebymakingHTTPrequeststoitandexaminingtheresponses,ratherthancheckingforspecificlow-levelinteractionswiththerequestandresponseobjects.Thisgivesusmoreflexibilitytochangeourimplementationandrefactorourcode,withoutneedingtochangethetests.Thus,ourtestscansupportthisprocess(bycatchingregressions)ratherthanhinderingit(byhavingtobeupdatedtomatchourimplementation).

Onotherplatforms,testingawholeapplicationcanbequiteaheavyweightprocess.Itispossibletostartupaserverinprocess,forexample,usingJettyinJavaorKatanain.NET.Newerapplicationframeworks,suchasSpringBootorNancyFx,alsomakethisprocesseasier.Thesearestilllikelytoberelativelyslowandresource-intensivetests,though.

InNode.js,startingupanapplicationserveriseasyandverylightweight.Wejustusethesamehttp.createServercallaswe'veseenbefore,andpassitanapplication.Totestourrouteinisolation,we'llbootstrapanewapplicationcontainingjustthisroute.Let'sseehowwecanusethistotestthedeleteendpointofourgamesroute.Weaddthefollowingcodeundertest/routes/games.js:

'usestrict';

consthttp=require('http');

constexpress=require('express');

constbodyParser=require('body-parser');

constexpect=require('chai').expect;

constgamesService=require('../../src/services/games.js');

constTEST_PORT=5000,userId='test-user-id';

describe('/games',()=>{

letserver;

constmakeRequest=(method,path,callback)=>{

http.request({

method:method,

port:TEST_PORT,

path:path

},callback).end();

};

before(done=>{

constapp=express();

app.use(bodyParser.json());

app.use((req,res,next)=>{

req.user={id:userId};next();

});

constgames=require('../../src/routes/games.js');

app.use('/games',games);

server=http.createServer(app).listen(TEST_PORT,done);

});

afterEach(()=>{

constgamesCreated=gamesService.availableTo("non-user");

gamesCreated.forEach(game=>game.remove());

});

after(done=>{

server.close(done);

});

describe('/:idDELETE',()=>{

it('shouldallowuserstodeletetheirowngames',done=>{

constgame=gamesService.create(userId,'test');

makeRequest('DELETE','/games/'+game.id,response=>{

expect(response.statusCode).to.equal(200);

expect(gamesService.createdBy(userId)).to.be.empty;

done();

});

});

});

});

Thismightseemlikequitealotofcode,butrememberthatwe'refiringupanentireapplicationhere.Also,mostofthiscodewillbereusedformultipletests.Let'sworkthroughwhatitdoes.

Thebeforecallbackcreatesourserver,justaswesawinChapter2,GettingStartedwithNode.js,listeningonaspecialportforusebyourtests.Italsosetsupsomestubmiddlewaretosimulateacurrentuserontherequest.TheafterEachcallbackclearsupanycreatedgames(aswesawbeforeinthetestofthegamesservice).Notethatsincewe'rerunninginthesameprocess,wecantriviallyinteractwiththesamedatalayerthatourapplicationisusing.Finally,theafterfunctionaskstheservertostoplisteningforconnections.

Thetestitselfisverysimple:wejustcreateagamesetbythecurrentuser(asinourservicetestsbefore)andthenissuearequesttodeleteit.ThismakesuseofourownmakeRequestfunction,whichsimplycallsthroughtoNode'shttp.request.Wecantheninspecttheresponseobjecttocheckfortheappropriatestatuscode,andchecktheserviceforthedesiredeffect.

Tip

WritingasynchronoustestsinMocha

NoticethatourtestandallofthecallbackstoMocha'shookfunctionsdiscussedabove(exceptforafterEach)takeadoneparameter.Thisisbecauseallofthesetestsperformsome

asynchronouswork.Mochamakesitveryeasytowriteasynchronoustestsorhooks:youjustmakeyourcallbackfunctiontakeasingleparameter(calleddonebyconvention),andcallitwhenprocessingiscomplete.Ifit'snotcalledwithinatimeout(whichdefaultsto2secondsbutcanbechanged),thenMochafailsthetest.

Let'srunourtestsagainusingthenpmtestcommand.Noticethatallofthetestsstillfinishveryquickly(tensofmillisecondsonmymachine),eventhoughwe'restartingupourwholeserver-sideapplication.Youmayalsonoticetheoutputisabitmessyduetologoutputfromtheserver.Wecaneasilysuppressthisbyupdatingapp.jsasfollows:

//app.use(favicon(path.join(__dirname,'public','favicon.ico')));

if(app.get('env')==='development'){

app.use(logger('dev'));

}

app.use(bodyParser.json());

The'env'propertyofanExpressapplicationcomesfromtheNODE_ENVenvironmentvariable(ordefaultstodevelopmentifthisisnotpresent).Thisisusefulfordifferentiatingbetweenproductionanddevelopmentenvironments.Sinceitdefaultstodevelopment,wealsoneedtosetittosomethingelseinordertosuppressthislogginginourtests.Wecandothisbyupdatingourtestscriptinpackage.jsonasfollows:

"scripts":{

"start":"node./bin/www",

"test":"setNODE_ENV=test&&nodenode_modules/mocha/bin/mocha

test/**/*.js"

},

SimplifyingtestsusingSuperAgentWhileourtestsarefast,andsettinguptheserverisquitestraightforward,wedohavequitealotofcodeformakingrequeststotheserverandhandlingresponses.Thiswouldbecomemorecomplexifweneededtomakeawidervarietyofrequests,orwereinterestedinmorethanjusttheresponsestatuscodeorheaders.

WecansimplifyourtestsbyusingalibrarythatprovidesasimplerAPIforcommunicatingwiththeserver.SuperAgent(https://visionmedia.github.io/superagent/)isaJavaScriptlibrarythatprovidesafluent,readablesyntaxformakingHTTPrequests.ThiscanbeusedforAjaxrequestsinthebrowser,orforrequestsinaNode.jsapplicationaswe'redoinghere.

We'llmakeuseofSuperAgentthroughalightweightwrappercalledSuperTest(https://github.com/visionmedia/supertest),whichmakestestingNode.js-basedHTTPapplicationsevenmoreconvenient.

First,weaddSuperTestintoourapplicationusingnpm,byrunningthefollowingonthecommandline:

>npminstallsupertest--save-dev

Nowwecanrewriteourtestsasfollows:

'usestrict';

constexpress=require('express');

constbodyParser=require('body-parser');

constrequest=require('supertest');

constexpect=require('chai').expect;

constgamesService=require('../../src/services/games.js');

constuserId='test-user-id';

describe('/games',()=>{

letagent,app;

before(()=>{

app=express();

app.use(bodyParser.json());

app.use((req,res,next)=>{

req.user={id:userId};next();

});

constgames=require('../../src/routes/games.js');

app.use('/games',games);

});

beforeEach(()=>{

agent=request.agent(app);

});

describe('/:idDELETE',()=>{

it('shouldallowuserstodeletetheirowngames',done=>{

constgame=gamesService.create(userId,'test');

agent

.delete('/games/'+game.id)

.expect(200)

.expect(()=>

expect(gamesService.createdBy(userId)).to.be.empty)

.end(done);

});

});

});

SuperTestandSuperAgenttakecareofstartinguptheserverforourapplication,andprovideamuchsimplerAPIformakingrequests.Notetheuseofarequestagent,whichrepresentsasinglebrowsersession.

SuperAgentprovidesanumberoffunctions(get,post,delete,andsoon)formakingHTTPrequests.Thesecanbechainedwithcallstotheexpectfunction(nottobeconfusedwithChai'sexpect)toverifypropertiesoftheresponse,suchasthestatuscode.Wecanalsopassinacallbacktomakespecificchecksabouttheresponse,orverifyside-effects(aswedointhepreviousexample).

Notethatitisimportanttoalwayscalltheendfunctiontomakesureanyexpectationerrorsarethrownandfailthetest.WecanpassMocha'sdonecallbacktoendthetestwhentherequestiscompleted.

Nowthatwe'vesimplifiedourtestcode,wecaneasilyaddmoretestsforourroutes.Forexample,let'saddsometeststocoverthenegativecasesofourdeleteendpoint:

it('shouldnotallowuserstodeletegamesthattheydidnotset',done

=>{

constgame=gamesService.create('another-user-id','test');

agent

.delete('/games/'+game.id)

.expect(403)

.expect(()=>expect(gamesService.get(game.id).ok))

.end(done);

});

it('shouldreturna404forrequeststodeleteagamethatnolonger

exists',done=>{

constgame=gamesService.create(userId,'test');

agent

.delete(`/games/${game.id}`)

.expect(200)

.end(function(err){

if(err){

done(err);

}else{

agent

.delete('/games/'+game.id)

.expect(404,done);

}

});

});

Full-stacktestingwithPhantomJSWehavenowwrittenunittestsforlogicatthecoreofourapplicationandintegrationtestsforourserver-sideroutes.Wedon'tyethaveanyautomatedteststhatcoverourviewsandclient-sidescriptsasourmanualtestingthroughoutthepreviouschaptersdid.

Wecanwriteunittestsforclient-sidescriptsusingMocha.However,allofourcurrentclient-sidescriptsinteractwiththeserver,soaren'tgoodcandidatesforunittesting.Ourmanualtestsarereallyfull-stacktestsofourwholeapplication,includingtheinteractionbetweentheserverandtheclient.

Inordertoachievethisinanautomatedtest,wewillneedtousesomeformofbrowserautomation.PhantomJSisaheadlessbrowserwithaJavaScriptAPIthatallowsustoautomateitdirectly.Wecanwriteasimpletestforourgamepageusingthis.

First,we'llinstallPhantomJSwithinourprojectbyrunningthefollowingonthecommandline:

>npminstallphantomjs-prebuilt--save-dev

Note

PhantomJSisnotaNode.jsmodule.Itisastandalone,headlesswebbrowser.Thenpmmoduleisjustaconvenientwayofinstallingitandmakingitadependencyoftheproject.PhantomJScannotbeinvokedfromNode.js,excepttoexecuteitasaseparatechildprocess.

Nowwecanimplementatestasfollows,underintegration-test/game.js:

(function(){

'usestrict';

varexpect=require('chai').expect;

varpage=require('webpage').create();

varrootUrl='http://localhost:3000';

withGame('Example',function(){

expect(getText('#word')).to.equal('_______');

page.evaluate(function(){

$(document).ajaxComplete(window.callPhantom);

});

page.sendEvent('keydown',page.event.key.E);

page.onCallback=verify(function(){

expect(getText('#word')).to.equal('E_____E');

expect(getText('#missedLetters')).to.be.empty;

page.sendEvent('keydown',page.event.key.T);

page.onCallback=verify(function(){

expect(getText('#word')).to.equal('E_____E');

expect(getText('#missedLetters')).to.equal('T');

console.log('Testcompletedsuccessfully!');

phantom.exit();

});

});

});

functionwithGame(word,callback){

...

}

functiongetText(selector){

returnpage.evaluate(function(s){

return$(s).text();

},selector);

}

functionverify(expectations){

returnfunction(){

try{

expectations();

}catch(e){

console.log('Testfailed!');

handleError(e.message);

}

}

}

functionhandleError(message){

console.log(message);

phantom.exit(1);

}

phantom.onError=page.onError=handleError;

}());

Makesuretheapplicationisrunning(usingnpmstart),thenexecutethetestbyrunningthefollowingonthecommandline:

>nodenode_modules/phantomjs-prebuilt/bin/phantomjsintegration-test/game.js

Let'stakealookthroughthecodetounderstandhowitworks.Notethatwe'rerunninginthebrowserenvironmenthereratherthanNode.js,sofallbacktotheECMAScript5syntax(forexample,varinsteadoflet,andnoarrowfunctions).

TheomittedwithGamemethod(whichyoucanfindinthebook'scompanioncode)usesPhantomJStoloadtheindexviewandsubmitanewgame,thenclearsPhantomJS'scookiesandopensthegameasanewuser,beforeinvokingthecallbackpassedtowithGame.

Inourtest,wecreateagametoguessthewordexample,theninvokeJavaScriptwithinthepagetomakeassertionsaboutitscontents.ThegetTextfunctionusesPhantomJS'spage.evaluatefunctiontorunsomeJavaScriptwithinthecontextofthepage,andreturnavalue.Notethatthecallbackfunctionpassedtopage.evaluatedoesnothaveaccesstothewiderexecutioncontext

ofourscript.Wecan,however,specifyadditionalargumentstothepage.evaluatecall,whichishowwepassintheselectorforjQuery.

Wethenusepage.evaluateagaintosetupacallbackeachtimeanAjaxrequestcompletes.Here,weusewindow.callPhantom,whichexecuteswithinthecontextofthepage,andtriggerspage.onCallback,whichexecuteswithinthecontextofourtest.

Finally,weusepage.sendEventtotriggerakeyboardeventinthebrowser.NotethatthisisnotthesameasusingpureJavaScriptwithinthebrowsertotriggeraDOMevent,butisaninstructiondirectlytoPhantomJStosimulatethekeypresseventasifithadcomefromtheuser.

Ifweputallthistogether,wegetthefollowing:

Weusepage.sendEventtosimulatepressingakeyboardkeyThiscausesourproductioncodetosendoffanAjaxrequestWhenthisrequestcompletes,window.callPhantomisinvokedinthecontextofthebrowserThiscausesPhantomJStoinvokeourpage.onCallbackfunctionWethenusejQuerywithinpage.evaluate(viagetText)toretrievevaluesfromthepage

Theremainingcontentsofthefile(verifyandhandleError)ensurethatPhantomJSwritesallerrorstotheconsoleandsetsanappropriateexitcodeinthecaseofafailure.

SummaryInthischapter,wehavelearnedhowtowriteunittestsinNode.js,usedMochaandChaitowritemoredescriptivetests,createdtestdoublesusingSinon.JS,writtenapplicationleveltestsusingSuperAgentandSuperTest,andimplementedafull-stacktestinPhantomJS.

Althoughwehavetestsateachlayerofourapplicationnow,wehaven'tyetcoveredallofourcode.Itwouldbeusefultofindanygapswhereweshouldwritemoretests.Wealsohavetoinvokeafewdifferentcommandstorunallofourunitandintegrationtests.Inthenextchapter,we'llseehowtoautomatetheseandotherprocessesaspartofacontinuousintegrationbuild.

Chapter7.SettingupanAutomatedBuildInthepreviouschapter,wetookamajorstepfromademoapplicationtoamaintainablecodebasebystartingtowriteautomatedtests.Anotherimportantcomponentofreal-worldsoftwareprojectsisbuildautomation.

Automatedbuildsallowawholeteamtoworkonaprojectinaconsistentmanner.Astandardizedwayofexecutingcommontasksmakesiteasierfornewdeveloperstogetstarted.Italsoavoidsannoyingissueswithdevelopersgettingdifferentresultsforspuriousreasons.

Inthischapter,wewillcoverthefollowingtopics:

ConfiguringanintegrationservertobuildandrunourtestsautomaticallySettingupanautomatedtaskrunnertosimplifytheexecutionofourtestsAutomatingmoretaskstohelpmaintaincodingstandardsandtestcoverage

SettingupanintegrationserverBuildandtestautomationallowcodechangestobeverifiedbyanintegrationserver,anautomatedserverindependentofindividualdevelopers'machines.Thishelpskeeptheprojectstablebycatchingerrorsorregressionsearlyon.Theintegrationservercanautomaticallyalertthedeveloperwhointroducedtheproblem.Theythenhaveachancetofixtheproblembeforeitcausesissuesfortherestoftheteamortheprojectasawhole.

BuildingthecodebaseandrunningtestsautomaticallyoneachcommitiscalledContinuousIntegration(CI).TherearemanyCI/buildserversavailable.Thesecanbeself-hostedorprovidedasathird-partyservice.ExamplesthatyoumayhaveusedbeforeincludeJenkins(formerlyHudson),AtlassianBamboo,JetBrainsTeamCity,andMicrosoft'sTeamFoundationServer.

We'regoingtobeusingTravisCI(https://travis-ci.org/),whichisahostedserviceforrunningautomatedbuilds.Itisfreeforusewithpublicsourcecoderepositories.InordertouseTravisCI'sfreeservice,weneedtohostourapplication'scodeinapublicGitHubrepository.

SettingupapublicGitHubrepositoryIfyouhaveyourownversionoftheexampleapplicationcodefromfollowingalongwiththebooksofar,andarealreadyfamiliarwithGitHub,youcanpushyourcodetoanewGitHubrepositoryofyourown.Otherwise,youcanforkoneoftheexamplechapterrepositories.

Usehttps://github.com/NodeJsForDevelopers/chapter06/ifyouwanttofollowalongwiththechangesinthischapter.ThiscontainstheexamplecodefromtheendofChapter6,TestingNode.jsApplications,whichwewillbuildoninthischapter.YoucancreateyourownforkofthisrepositoryusingtheForkbuttononGitHub.Thisshouldbevisibleatthetop-rightofthescreenwhenvisitingtheURLmentionedearlier:

ThiswillcreateanewrepositoryunderyourownGitHubaccount,usingtheexamplecodeasastartingpoint.

Note

Thisisallyouneedtogetstartedinthischapter.However,ifyouarenotalreadyfamiliarwithGitand/orGitHubandwouldliketoknowmore,youcanfindmoreinformationathttps://help.github.com/.

BuildingaprojectonTravisCIWe'llnowsetupabuildforourapplicationonTravisCI.Ifyoucreatedyourownpublicrepositoryintheprevioussection,youcantrythisoutforyourself.Visithttps://travis-ci.organdsigninwithGitHub.Youshouldseeaprofilepagelistingyourrepositories.Enabletherepositoryyoujustcreated.

WehavetocreateasimpleconfigfiletotellTravisCIinwhatenvironment(s)tobuildourapplication.Createafileintherootoftheprojectasfollows(notetheleadingdotinthefilename.travis.yml):

language:node_js

node_js:

-6

-4

ThistellsTravisCItobuildourprojectwiththecurrentstableandlong-termsupportversionsofNode.js(atthetimeofwriting).Ifyou'refamiliarwithGit,youcanmakethischangeinalocalcloneofyourrepository,commit,andpushittomaster.Ifyou'renewtoGit,theeasiestwaytocreatethisfileistonavigatetoyourrepositoryonhttps://github.comandclickontheNewfilebutton.Thiswillopenaweb-basededitorfromwhichyoucancreateandcommitthefile.

Onceyouhaveaddedthisfiletoyourrepository,visithttps://travis-ci.orgagain.Youshouldnowseeapassingbuildforyourrepository:

TravisCIbuiltourprojecttwice,onceforeachversionofNode.jsthatwespecified.Ifyouclickoneitherbuildyoucanseethecommand-lineoutput.NoticethatTravisCIautomaticallyranourtestsusingthestandardnpmtestcommand.

AutomatingthebuildprocesswithGulpIt'sgreatthatTravisCIrunsourtestsautomatically.Butthat'snottheonlytaskwewanttoautomate.Ofcourse,asJavaScriptisaninterpretedlanguage,wedon'thaveacompilestepinourbuildprocess.Thereareothertaskswewanttocarryoutthough,forexample,checkingourcodestyle,runningintegrationtests,andgatheringcodecoverage.Wecanmakeuseofabuildtooltoautomatethesetasksandallowustoruntheminaconsistentmanner.YoumayhaveusedMSBuildforthisin.NETbeforeorJavatoolssuchasMavenorGradle.

ThereareseveraldifferentbuildtoolsavailableforNode.js.ThetwomostpopularbyfarareGruntandGulp.Bothhavelargecommunitiesandanextensiverangeofpluginsforperformingdifferentoperations.Grunt'smodelhaseachoperationreadinginfilesandwritingbacktothefilesystem.GulpusesNode.jsstreamstopipeprocessingfromoneoperationtothenext.

Grunt'smodelisslightlysimplerandmaybeeasiertogetstartedwith,especiallyifyouhavemodestbuildrequirements.Gulp'smodelispotentiallyfasterforsometypesoftaskandcanreducetheamountofbuildconfigurationcodeyouneedtowrite.Bothareexcellent,well-supportedbuildtools.We'llbeusingGulp,buteverythingwedointhischaptercouldbeachievedwithGruntaswell.

RunningtestsusingGulpWefirstneedtoinstallGulp,bothglobally(toaddittoourpath)andintotheproject.ThenweaddGulppluginsforcontrollingMochaandenvironmentvariables:

>npminstall-ggulp-cli

>npminstallgulp@~3.x--save-dev

>npminstallgulp-mocha--save-dev

>npminstallgulp-env--save-dev

WenowaddaconfigurationfileforGulptoourproject.Gulpwilllookforafilewiththisnamebyconventionasgulpfile.js:

'usestrict';

constgulp=require('gulp');

constmocha=require('gulp-mocha');

constenv=require('gulp-env');

gulp.task('test',function(){

env({vars:{NODE_ENV:'test'}});

returngulp.src('test/**/*.js')

.pipe(mocha());

});

gulp.task('default',['test']);

Thiscreatesatesttaskandmakesanemptydefaulttasktorunit.The'default'tasknameisspecialandwillbeinvokedwhenwerungulpfromthecommandline.Wecannowremoveourtestscriptfrompackage.jsonandupdateour.travis.ymlfiletorunGulp:

language:node_js

before_script:

-npminstall-ggulp

script:gulp

node_js:

-6

-4

Thishasn'tgainedusmuchyet.Wenowjusthaveaslightlyshortercommandtoexecuteourtests.However,theuseofabuildtoolwillbecomemorevaluableasweaddmoretaskstoautomate.Let'slookatsomeoftheotherprocesseswemaywanttomakepartofourbuild.

CheckingcodestylewithESLintAlthoughwedon'tneedacompiler,wecanstillbenefitfromhavingthecomputerperformstaticanalysisofourcode.Lintingtoolsarecommoninmanylanguagesforspottingcommonprogrammingerrorsthatmayleadtosubtlebugsorconfusingcode.YoumaybefamiliarwithCodeRush,StyleCop,andothersfor.NET,orCheckStyle,Findbugs,Sonar,andothersforJava.

We'llbeusingaJavaScript/ECMAScriptlintingtoolcalledESLint.Let'sfirstinstallitglobally:

>npminstall-geslint

NowcreateaconfigfiletotellESLintwhatrulestouseas.eslintrc.json:

{

"extends":"eslint:recommended",

"env":{

"node":true,

"es6":true,

"mocha":true,

"browser":true,

"jquery":true

},

"rules":{

"semi":[2,"always"],

"quotes":[2,"single"]

}

}

Here,wetellESLinttouseitsstandardrecommendedrulesfortheenvironmentsthatweareusinginourscripts.Wealsotellittocheckforsemicolonsattheendsofstatementsandtoprefersinglequotes.YoucanrunESLintasfollows:

>eslint**/*.js

ESLintoutputsanyerrorsitfinds,includingthefollowing:

Anunusedfaviconlocalvariableinapp.jsTheunusednextparameterinvariousmiddlewarefunctionsTheuseofconsole.loginourPhantomJSintegrationtestTheuseofthephantomvariableinourPhantomJSintegrationtest

Thefirstoftheseistrivialtosolve:wecanjustremovethevariabledeclaration(thiswascreatedforusbytheexpressapplicationtemplateinChapter2,GettingStartedwithNode.js).Wecoulddothesameforthenextparametersonourmiddlewarefunctions.However,Iprefermiddlewarefunctionstohaveastandardandeasilyidentifiablesignature.Insteadofremovingthisparameter,wecantellESLinttoignorethisparticularparameterasfollows:

"rules":{

"semi":[2,"always"],

"quotes":[2,"single"],

"no-unused-vars":[2,{"argsIgnorePattern":"next"}]

}

ThelasttwobulletpointsbothrelatetoourPhantomJSintegrationtest.Thisisquiteaspecialfile,soherewe'llchangeESLint'sbehaviorforthisfilespecifically,usingacommentdirective.Wecanaddthefollowingdirectivesattheverytopoftheoffendingfile,integration-test/game.js:

/*eslint-envphantomjs*/

/*eslint-disableno-console*/

ThefirstofthesedirectivestellsESLintthatthisscriptfilewillruninthePhantomJSenvironment,wherethephantomvariablewillbeprovidedforus,soESLintdoesnotneedtowarnusagainstreferencingit.Theseconddirectivedisable'sESLint'sruleagainstusingconsolelogging.

IfyourunESLintagain,youshouldfindthattheerrorslistedpreviouslyhavedisappeared.Anyremainingerrorsshouldbesmallerissuessuchasmissingsemicolonsorinconsistentuseofquotes.Theseshouldbequicktofixmanually,butinfact,ESLintcandothisforus,aswe'llseeinthenextsection.

AutomaticallyfixingissuesinESLintESLintisabletoautomaticallycorrectsomeoftheissuesitfinds.IfESLintisnotcurrentlyreportinganyerrors,tryremovingasemicolonfromoneoftheproject'ssourcefiles.RunESLintandyoushouldseeanerrorforthis.

NowrunESLintwiththe--fixoptionasfollows:

>eslint**/*.js--fix

ESLintreplacesthesemicolonforus.NotallofESLint'srulescanbefixedinthisway,butmanyofthemcan.Itdependsonwhetherarule'serrorsalwayshaveasingleunambiguousfix.Thefulllistofrules,includingwhichonesarefixable,canbefoundontheESLintsiteathttp://eslint.org/docs/rules/.

YoushouldnowbeabletorunESLintwithnoerrorsorwarnings.ESLintisnowreadytopickuperrorsinanynewcodethatwewrite.

RunningESLintfromGulpIt'sslightlymessytospecifyspecialexclusionsforourPhantomintegrationtest.It'salsounfortunatethatwe'reenablingtheNode.js,Mocha,browser,andjQueryenvironmentsglobally.TheMochaenvironmentisonlyneededforourtestcode.ThebrowserandjQueryenvironmentsareonlyneedforourclient-sidecode,wheretheNode.jsenvironmentisnotneeded.

ThiswouldbeeasiertomanageifweranESLintseparatelyondifferentsetsoffiles.Thiswouldstarttobecometediousanderror-proneifwediditmanually.Butit'sagreatusecaseforabuildtool.WecansetupseparateESLintprofilesfordifferentsetsoffilesusingGulp.First,installtheGulpESLintplugin:

>npminstallgulp-eslint--save-dev

NowwecancreateGulptaskstolinteachsetofsources.Bydefault,thegulp-eslintpluginusesrulesfromour.eslintrc.jsonfile.So,wecancutthisdowntojusttherulesthatarerelevanttoallsources:

{

"extends":"eslint:recommended",

"rules":{

"no-unused-vars":[2,{"args":"after-used"}],

"quotes":[2,"single"],

"semi":[2,"always"]

}

}

WecanthenspecifytherelevantrulesorenvironmentsforeachsetofsourcesintheirownGulptask.Thisalsoallowsustoremovethespecialdirectivecommentsfromthetopofourintegrationtestscript:

consteslint=require('gulp-eslint');

gulp.task('lint-server',function(){

returngulp.src(['src/**/*.js','!src/public/**/*.js'])

.pipe(eslint({

envs:['es6','node'],

rules:{

'no-unused-vars':[2,{'argsIgnorePattern':'next'}]

}

}))

.pipe(eslint.format())

.pipe(eslint.failAfterError());

});

gulp.task('lint-client',function(){

returngulp.src('src/public/**/*.js')

.pipe(eslint({envs:['browser','jquery']}))

.pipe(eslint.format())

.pipe(eslint.failAfterError());

});

gulp.task('lint-test',function(){

returngulp.src('test/**/*.js')

.pipe(eslint({envs:['es6','node','mocha']}))

.pipe(eslint.format())

.pipe(eslint.failAfterError());

});

gulp.task('lint-integration-test',function(){

returngulp.src('integration-test/**/*.js')

.pipe(eslint({

envs:['browser','phantomjs','jquery'],

rules:{'no-console':0}

}))

.pipe(eslint.format())

.pipe(eslint.failAfterError());

});

Finally,wewireupthedependenciesbetweenourtasks:

gulp.task('test',['lint-test'],function(){

env({vars:{NODE_ENV:'test'}});

returngulp.src('test/**/*.js')

.pipe(mocha());

});

gulp.task('lint',[

'lint-server','lint-client','lint-test','lint-integration-test'

]);

gulp.task('default',['lint','test']);

Here,wemakethetesttaskdependonlint-testandcreateanewoveralllinttasktorunalloftheothersaspartofthedefaultbuild.TryrunningGulpandobservetheoutput.Notethatitkicksoffallthelinttasksinparallel,butwaitsforlint-testtocompletebeforerunningtests.Bydefault,Gulpwillruntasksconcurrentlyifpossible.Ifataskreturnsastream(theobjectobtainedfromgulp.src)attheend,Gulpisabletousethistodetectwhenthetaskfinishes.Gulpwillwaitforatasktofinishbeforestartinganytasksthatdependonit.

ToseehowESLintfailuresaffectGulp,let'saddanotherESLintruletoensuretheuseofJavaScript'sstrictmode,asdescribedinChapter3,AJavaScriptPrimer.Thefollowingcodeisfrom.eslintrc.json:

{

"extends":"eslint:recommended",

"rules":{

"no-unused-vars":[2,{"args":"after-used"}],

"quotes":[2,"single"],

"semi":[2,"always"],

"strict":[2,"safe"]

}

}

ESLintiscleverenoughtomakeuseofthespecifiedenvironmentforeachsetoffilestoworkouthowstrictmodeshouldbeapplied:atthetopoffunctionsforclient-sidescriptsandgloballyfor

filesthatwillbecomeNode.jsmodules.Italsospotswhenweunnecessarilyspecifystrictmodemultipletimes,globallyorinnestedfunctions.

WhenyouexecuteGulp,noticethatfailuresintheESLinttaskspreventthedependenttesttasksfromrunning.Ifyoufixthestrictmodeerrors,thenGulpwillrunsuccessfullyagain.

GatheringcodecoveragestatisticsAlthoughwehavesometestsforourapplication,theyarecertainlynotyetcomprehensive.Itwouldbeusefultobeabletoseewhatpartsofourcodearecoveredbytests.Forthis,we'lluseIstanbul,aJavaScriptcodecoveragetool.First,installthegulp-instanbulplugin:

>npminstallgulp-istanbul--save-dev

NowweneedtoaddaGulptasktoinstrumentourproductioncodeforcoverage:

constistanbul=require('gulp-istanbul');

...

gulp.task('instrument',function(){

returngulp.src('src/**/*.js')

.pipe(istanbul())

.pipe(istanbul.hookRequire())

});

Finally,weneedtoupdateourtesttasktooutputacoveragereportandfailthebuildifwearebelowourthreshold:

gulp.task('test',['lint-test','instrument'],function(){

gulp.src('test/**/*.js')

.pipe(mocha())

.pipe(istanbul.writeReports())

.pipe(istanbul.enforceThresholds({

thresholds:{global:90}

}));

});

Now,whenwerunGulp,threenewresultsoccur:

AcoveragesummaryappearsonthecommandlineAsetofcoveragereportsappearunderthecoveragefolderThebuildfailsbecausewearebelowthecoveragethreshold

Thebuildsummaryonthecommandlineisveryuseful.ThereisevenmoredetailintheHTMLreportthatappearsatcoverage/lcov-report/index.html(intheprojectdirectory).

Althoughweneedtoimproveourtestcoverage,wedon'twanttoleaveourbuildfailing.Fornow,we'llsetthecoveragetargetjustbelowourcurrentlevelsoitdoesn'tdropfurther.Wecandothiswiththeoptionspassedtoistanbul.enforceThresholds:

gulp.task('test',['lint-test','instrument'],function(){

returngulp.src('test/**/*.js')

.pipe(mocha())

.pipe(istanbul.writeReports())

.pipe(istanbul.enforceThresholds({

thresholds:{

global:{

statements:70,

branches:50

}

}

}));

});

RunningintegrationtestsfromGulpGulptasksarejustordinaryJavaScriptfunctions,socancontainanyfunctionalitywelike.Let'slookatamorecomplexusecase.We'llcreateataskthatstartsupourserver,runsintegrationtests,andthenclosestheserver.Forthis,we'llneedtheGulpShellplugin:

>npminstallgulp-shell--save-dev

First,weupdateourintegrationtestscriptsothatwecanpassintheportnumberofthetestserver.ThismakesuseofthePhantomJS'system'moduleasfollows(inintegration-test/game.js):

varrootUrl='http://localhost:'+

require('system').env.TEST_PORT||3000;

NowwecandefineaGulptasktoruntheserverandtheintegrationtest:

constshell=require('gulp-shell');

...

gulp.task('integration-test',

['lint-integration-test','test'],(done)=>{

constTEST_PORT=5000;

letserver=require('http')

.createServer(require('./src/app.js'))

.listen(TEST_PORT,function(){

gulp.src('integration-test/**/*.js')

.pipe(shell('nodenode_modules/phantomjs-prebuilt/bin/phantomjs

<%=file.path%>',{

env:{'TEST_PORT':TEST_PORT}

}))

.on('error',()=>server.close(done))

.on('end',()=>server.close(done))

});

});

Thislaunchestheapplicationandthenmakesuseofthegulp-shellplugintoexecuteourintegrationtestscripts.Finally,wemakesureweclosetheserverwhendone,passinginGulp'sasynccallback.Likereturningastream,usingthiscallbackallowsGulptoknowwhenthetaskhascompleted.

Wemakethistaskdependonthetesttasksothattheydon'tinterferewithoneanother.Wedon'tmakethispartofourdefaulttaskasit'samoreheavyweightoperation.Wedowantittorunonourbuildserverthough,sowe'lladditto.travis.ymlalongwiththedefaulttask:

language:node_js

before_script:

-npminstall-ggulp

script:gulpdefaultintegration-test

node_js:

-5

-4

Now,ifwepushtotheremotemaster,TravisCIwillrunstaticanalysisonourcode,executeallofourunitandintegrationtests,andchecktheunittestcoverage.

SummaryInthischapter,wehavesetupanintegrationbuildusingTravisCI,addedstaticanalysisofourcodeusingESLint,automatedourtestsandothertasksusingGulp,andstartedmeasuringourtestcoverageusingtheIstanbultool.

Nowthatwehavetheinfrastructureinplaceforstabledevelopment,wecanstartexpandingourproject.Inthenextchapter,we'llintroducepersistentdatastorestotheapplication.

Chapter8.MasteringAsynchronicityOurJavaScriptprimer(Chapter3,AJavaScriptPrimer)coveredalltheimportantconceptstoletusstartbuildingourapplication.ButthereisonefundamentalaspectofJavaScriptprogrammingworthexploringinmoredetail:asynchronicity.

Chapter1,WhyNode.js?,discussedtheasynchronousprogrammingmodelofNode.js.ItdescribedtheconsistentapproachusedthroughoutNode.jsAPIsandthird-partylibraries.Recallthateachasynchronousmethodtakesacallbackfunctionthatgetspassederrorandresultarguments,forexample,thefs.statfunctionwesawinChapter1,WhyNode.js?:

fs.stat('/hello/world',function(error,stats){

console.log('Filelastupdatedat:'+stats.mtime);

});

However,thecallbackpatternhassomeweaknesses.Performingerrorhandlingandcombiningresultsfrommultipleasynchronousoperationscanbecomequiteclumsy.TherearealternativeasynchronouspatternsavailableinJavaScriptthataddresstheseissues.Theideaofmultiplecompetingpatternsmightseemworryinginitself,though.HavingasingleconsistentapproachwasoneofthebenefitsofNode.jsdiscussedinChapter1,WhyNode.js?.

WeshouldalsorevisittheideaofNode.jsAPIsandlibrariesbeingasynchronousthroughout.Weneedtoconsiderhowthisappliestoourowncode.Thisisnotjustsomethingweneedtoworryaboutifwritingamoduleforusebyathird-party.Evenwithinourownapplications,mostmoduleswillneedtoexposetheirfunctionalitythroughanasynchronousinterface.Ifnot,weseverelylimitthefreedomofhowweimplementthesemodules.

Inthischapter,wewillcoverthefollowingtopics:

IntroducingasynchronousinterfacestoourownmodulesObservingsomeoftheweaknessesofthecallbackpatternRefactoringawayfromcallbackstomakeourasynchronouscodemorereadableSeeinghowwecanstillbenefitfromtheconsistencyofNode.js'sasynchronousprogrammingmodel

UsingthecallbackpatternforasynchronouscodeLet'slookatoneofthemethodsfromourgamesservice:

module.exports.get=(id)=>games.find(game=>game.id===id);

Theinterfaceofthisfunctionissynchronous:youcallitandgetavalueback.Chapter4,IntroducingNode.jsModules,introducedthegamesserviceasthemoduleresponsibleforhowwestoreourgames.Theinterfaceshouldn'tneedtochangeifwechangethestorageimplementation.Thisisn'tquitethecaseatthemoment,though.

Asdiscussedbefore,mostNode.jslibrariesareasynchronous.Synchronousinterfacescan'tmakeuseofasynchronousimplementations.Let'ssaythegetfunctionwantstomakeuseofanasynchronousmethodinathird-partydatastorelibrary.Whatwouldthatlooklike?Thecommentsinthefollowing(non-working)codedescribetheproblem:

module.exports.get=(id)=>{

datastore.getById(id,(err,result)=>{

//Resultavailable,butoutermethodhasalreadyreturned

});

return???;//Needtoreturnhere,buthavenoresultyet

};

Thisisaproblemingeneral,notjustinJavaScript.Inotherplatforms,youcoulddelayreturninguntiltheasynchronousoperationhascompleted.Thisturnsanasynchronousoperationintoablockingoperation.InNode.js(andotherJavaScriptenvironments),blockinginthiswayisnotanoption.Itwouldbeincompatiblewiththesingle-threaded,non-blocking,event-drivenexecutionmodel.

ExposingthecallbackpatternToallowourgamesservicetobeabletomakeuseofasynchronouslibraries,weneedtogiveitanasynchronousinterface.NotethatalmostalllibrariesintheNode.jsecosystemareasynchronous.Iftheyweren't,theywouldbelimitedinthesamewayasourgamesservicecurrentlyis.

Wecanrewritetheinterfaceofourgetfunctiontofollowthestandardasynchronouscallbackpattern.Let'sseewhateffectthishasonusinganasynchronousthird-partydatastorelibrary(again,thisisnon-workingcode,withafictionaldatastoreobject):

module.exports.get=(id,callback)=>{

datastore.getById(id,(err,result)=>{

//Cannowmakeuseoftheresultbypassingtothecallback

callback(err,result);

}

//Nolongerneedtoreturnhere

}

Ofcourse,inthiscasewecouldsimplifytheprecedingcodeasfollows:

module.exports.get=(id,callback)=>{

datastore.getById(id,callback);

}

Ingeneral,though,wemightwanttodosomemoreprocessingoftheresultfromathird-partylibrary.Soourfunctionmightlookmorelikethis:

module.exports.get=(id,callback)=>{

datastore.getById(id,(err,result)=>{

if(err){

callback(err);

}else{

callback(null,processResult(result));

}

}

}

AssumingprocessResultisinternaltoourmodule,it'sfineforittohaveasynchronousinterfacefornow.Ifitneedstodoasynchronousworklater,wecanchangeitsinterfacewithoutaffectingtheconsumersofourmodule.

Ourgamesservicemodule'spublicinterfacedoesneedtobeentirelyasynchronous,though.We'renotactuallychangingtheimplementationofthemoduleyet.Thismakesupdatingtheinterfacequitestraightforward.Wecanmakethefollowingchangesinsrc/services/games.js:

'usestrict';

constgames=[];

letnextId=1;

classGame{

...

remove(callback){

games.splice(games.indexOf(this),1);

callback();

}

}

module.exports.create=(userId,word,callback)=>{

constnewGame=newGame(nextId++,userId,word);

games.push(newGame);

callback(newGame);

};

module.exports.get=(id,callback)=>

callback(null,

games.find(game=>game.id===parseInt(id,10)));

module.exports.createdBy=(userId,callback)=>

callback(null,games.filter(game=>game.setBy===userId));

module.exports.availableTo=(userId,callback)=>

callback(null,games.filter(game=>game.setBy!==userId));

Notethatthisisslightlyunrealistic,though.Controlwouldnormallyreturntothecallerbeforeanasynchronousmethodcompletes.Wecanachievethisbyusingprocess.nextTicktoscheduletheexecutionofthecallbackonthenexttickoftheeventloop(refertoChapter1,WhyNode.js?,ifyouwantarefresherontheeventloop):

'usestrict';

constgames=[];

letnextId=1;

constasAsync=(callback,result)=>

process.nextTick(()=>callback(null,result));

classGame{

...

remove(callback){

games.splice(games.indexOf(this),1);

asAsync(callback);

}

}

module.exports.create=(userId,word,callback)=>{

letgame=newGame(nextId++,userId,word);

games.push(game);

asAsync(callback);

};

module.exports.get=(id,callback)=>

asAsync(callback,

games.find(game=>game.id===parseInt(id,10)));

module.exports.createdBy=(userId,callback)=>

asAsync(callback,games.filter(game=>game.setBy===userId));

module.exports.availableTo=(userId,callback)=>

asAsync(callback,games.filter(game=>game.setBy!==userId));

Updatingtherestofourapplicationtoconsumethisasynchronousinterfaceisatrickiertask.Thisiswhyitisworthalwayswritingmoduleinterfacestobeasynchronousfromthestart.Weshoulddefinitelyaddressthisbeforeexpandingourapplicationanyfurther.

ConsumingasynchronousinterfacesThegamesserviceiscalledbythegamesroute,theindexroute,andbyourtests.Let'slookatthecorrespondingchangestoeachoftheseinturn.Thefollowingcodeisfromsrc/routes/games.js:

'usestrict';

constexpress=require('express');

constrouter=express.Router();

constservice=require('../service/games.js');

router.post('/',function(req,res,next){

letword=req.body.word;

if(word&&/^[A-Za-z]{3,}$/.test(word)){

service.create(req.user.id,word,(err,game)=>{

if(err){

next(err);

}else{

res.redirect(`/games/${game.id}/created`);

}

});

}else{

res.status(400).send('Wordmustbeatleastthreecharacterslongand

containonlyletters');

}

});

constcheckGameExists=function(id,res,onSuccess,onError){

service.get(id,function(err,game){

if(err){

onError(err);

}else{

if(game){

onSuccess(game);

}else{

res.status(404).send('Non-existentgameID');

}

}

});

};

router.get('/:id',function(req,res,next){

checkGameExists(

req.params.id,

res,

game=>{...},

next);

});

router.post('/:id/guesses',function(req,res,next){

checkGameExists(

req.params.id,

res,

game=>{...},

next);

});

router.delete('/:id',function(req,res,next){

checkGameExists(

req.params.id,

res,

game=>{

if(game.setBy===req.user.id){

game.remove((err)=>{

if(err){

next(err);

}else{

res.send();

}

});

}else{

res.status(403).send('Youdon'thavepermission...');

}

},

next);

});

router.get('/:id/created',function(req,res,next){

checkGameExists(

req.params.id,

res,

game=>res.render('createdGame',game),

next);

});

module.exports=router;

Inthiscase,thechangesarestraightforward.Eachcalltoagamesservicefunctionnowpassesinacallback.Thecallbackcontainsthelogicthatusedtofollowthecalltothegamesservicefunction.Eachcallbackalsoneedstohandlethepossibilityofanerrorvalue.Inthiscase,wesimplypassittotheExpressnextcallbacksoitwillbehandledbyourglobalerrorhandler.

Althoughthesechangesarestraightforward,theyhaveintroducedsomerepetitiveboilerplatetoourcode.Thisisevenmoreofaproblemintheindexroute;takealookatthecodefromsrc/routes/index.js:

varexpress=require('express');

varrouter=express.Router();

vargames=require('../service/games.js');

router.get('/',function(req,res,next){

games.createdBy(req.user.id,(err,createdGames)=>{

if(err){

next(err);

}else{

games.availableTo(req.user.id,(err,availableGames)=>{

if(err){

next(err);

}else{

res.render('index',{

title:'Hangman',

userId:req.user.id,

createdGames:createdGames,

availableGames:availableGames,

partials:{createdGame:'createdGame'}

});

}

});

}

});});

module.exports=router;

Here,weneedtocombinetheresultoftwodifferentasynchronouscalls.Thisleadstonestedcallbacks.Wealsohavetorepeattheerror-handlingcodeateachstage.Notealsothatweonlystartthesecondasynchronousoperationafterthefirstonecompletes.Itwouldbebettertostarttheoperationsinparallel.

Recallthat,whileJavaScriptitselfissingle-threaded,asynchronousoperationsmayperformworkinparallel,forexample,network,disk,andotherI/Ooperations.Runningmultipleoperationsinparallelwouldneedevenmorecomplicated(anderror-prone)boilerplatecode.Foranexampleofhowthismightwork,considerthechangestomakethebeforeEachfunctioninthegamesservicetestasynchronous.Thefollowingcodeisfromsrc/test/services/games.js:

describe('Gameservice',function(){

letfirstUserId='user-id-1';

letsecondUserId='user-id-2';

beforeEach(function(done){

service.availableTo('not-a-user',(err,gamesAdded)=>{

letgamesDeleted=0;

if(gamesAdded.length===0){

done();

}

gamesAdded.forEach(game=>{

game.remove(()=>{

if(++gamesDeleted===gamesAdded.length){

done();

}

});

});

});

});

...

});

Here,weneedtomakeanunknownnumberofcallstotheasynchronousremovemethod.Thedonecallbackmustbeinvokedwhentheyareallcomplete.Thereareseveralwaysofachievingthis,buttheyallinvolveadditionalboilerplate.Theapproachhereisthesimplestpossible,

keepingcountofthenumberofcompleteoperations.Alsonotethatweareomittingerrorhandling,sincethisistestcode.Inproductioncode,wewouldhavetoworryabouterrorhandlingaswell,makingthingsevenmorecomplicated.

Note

Thereareotherchangestotheteststomakeuseofthenewasynchronousinterfaceofthegamesservice.Theyareexcludedhereforbrevity.Theyaresimilartothechangesinindex.js.Youcanseeafullsetofchangesbyviewingthischapter'sfirstcommitintheGitrepositoryathttps://github.com/NodeJsForDevelopers/chapter08.

Thisallseemsquiteunsatisfactory.Ourcodehasbecomemorecomplicated,repetitive,andhardertoread.Fortunately,wecanaddresstheseissuesbyusingadifferentapproachtowritingasynchronouscode.

WritingcleanerasynchronouscodeusingpromisesPromisesareanalternativepatterntocallbacksforwritingasynchronouscode.Apromiserepresentsanoperationthathasn'tcompletedyetbutisexpectedtodosointhefuture.Asthenamepromisesuggests,apromiseisacontracttoeventuallyprovideavalueorareasonforfailure(thatis,anerror).YoumayalreadybefamiliarwiththispatternfromTasksin.NETorFuturesinJava.Apromisehasthreepossiblestates:

pendingrepresentsanin-progressoperationfulfilledrepresentingasuccessfuloperation,witharesultvaluerejectedrepresentinganunsuccessfuloperation,withafailurereason

Whenexecutingasingleoperation,thecallback-basedandpromise-basedapproachesappearquitesimilar.Thepowerofpromisescomeswhencombiningasynchronousoperations.

Consideranexamplewherewehaveasynchronouslibraryfunctionsforobtaining,processing,andaggregatingdata.Wewanttoperformtheseoperationsinturnthendisplaytheresult,handlingerrorsaswego.Usingcallbacks,itmightlooklikethis(innon-runnable,fictionalcode):

lib.getInitialData(function(e,initialData){

if(e){

console.log('Error:'+e);

}else{

lib.processData(initialData,(e,processedData)=>{

if(e){

console.log('Error:'+e);

}else{

lib.aggregateData(processedData,(e,aggregatedData)=>{

if(e){

console.log('Error:'+e);

}else{

console.log('Success!Result='+aggregatedData);

}

});

}

});

}

});

Thishasmanyofthesameproblemsweencounteredinourowncodeintheprevioussection:nestedcallbacks,extraboilerplate,andrepetitiveerror-handling.Ifthesefunctionsinsteadreturnedpromises,theequivalentoftheabovecodewouldbeasfollows:

lib.getInitialData()

.then(lib.processData)

.then(lib.aggregateData)

.then(function(aggregatedData){

console.log('Success!Result='+result);

},function(error){

console.log('Error:'+error);

});

Thethenfunctionappliesafunctiontotheresultingvalueofapromise,returninganewpromise.Inthisway,weconstructachainofpromisesrepresentingaseriesofoperations.

Thethenfunctiontakestwoarguments,whicharebothcallbacks.Iftheasynchronousoperationreturnsanerror,thesecondargumentwillbeinvokedinstead.Intheaboveexample,ifthelibrary.aggregateDatacallfails,thenwewillloganerror.

Ifthesecondthencallbackparameterisomitted,anyerrorspropagatealongthechainofpromises.Intheaboveexample,thismeansthatifthelibrary.processDatacallfails,thenlibrary.aggregateDatawillnotbecalledandourerror-loggingcallbackwillstillbeinvoked.

Ifyouonlycareabouttheerrorcase,youcanjustspecifyanerrorcallbackusingthecatchfunctioninsteadofthen.Youcanalsousethistogetherwithpropagationtorewritetheprecedingcodemoreclearly:

library.getInitialData()

.then(library.processData)

.then(library.aggregateData)

.then(function(aggregatedData){

console.log('Success!Result='+result);

})

.catch(function(error){

console.log('Error:'+error);

});

Here,errorsatanypointpropagatetoafinalpromisewhichwecheckforerrors.Notethatthisrewrittenversionwouldalsocatchanyerrorsthrownbyoursuccess-loggingcallback,whichtheprecedingversionwouldnothavedone.Youshouldalwayscallcatchattheendofapromisechain,unlessyouarereturningtheresultingpromiseobjecttobeconsumedelsewhere.

Implementingpromise-basedasynchronouscodeLet'sapplythepromisepatterntoourexistingapplication.First,we'llneedtoupdateourgameserviceAPItoexposepromisesinsteadofcallbacks.Asbefore,thisisstraightforwardsinceourgameservicedoesn'tactuallyuseanyasynchronousoperationsinitsimplementation(yet).Apromised-basedversionofourgamesservicelookslikethefollowing(insrc/services/games.js):

'usestrict';

constgames=[];

letcurrentId=1;

classGame{

...

remove(){

games.splice(games.indexOf(this),1);

returnPromise.resolve();

}

}

module.exports.create=(userId,word)=>{

constnewGame=newGame(nextId++,userId,word);

games.push(newGame);

returnPromise.resolve(newGame);

};

module.exports.get=(id)=>

Promise.resolve(

games.find(game=>game.id===parseInt(id,10)));

module.exports.createdBy=(userId)=>

Promise.resolve(games.filter(game=>game.setBy===userId));

module.exports.availableTo=(userId)=>

Promise.resolve(games.filter(game=>game.setBy!==userId));

Creatingapromise-basedinterfaceisevensimplerthanacallback-basedone.WecancreateapromiseforanalreadyknownvalueusingthePromise.resolve()function.Eachfunctioninourgamesservicelooksmuchliketheoriginalsynchronousversion,justwithanextracalltoPromise.resolve.

Note

IfyoupassapromiseargumenttoPromise.resolve,thenyougetbackapromisethatbehavesliketheoriginalargument.Ifyoupassanyothervalue,yougetanalreadyresolvedpromiseforthatvalue.Thiscanbeusefulifyouneedtooperateonavariablethatmightbeapromiseoravalue.YoucanpassittoPromise.resolve,thentreatitconsistentlyasapromise.

Consumingthepromisepattern

Nowweneedtoupdatetherestofourcodebasetousepromises.Let'slookthroughthesamefilesasbefore,startingwiththegamesroute.Seethefollowingcodefromsrc/routes/games.js:

'usestrict';

constexpress=require('express');

constrouter=express.Router();

constservice=require('../service/games.js');

router.post('/',function(req,res,next){

letword=req.body.word;

if(word&&/^[A-Za-z]{3,}$/.test(word)){

service.create(req.user.id,word)

.then(game=>

res.redirect(`/games/${game.id}/created`))

.catch(next);

}else{

res.status(400).send('Wordmustbeatleastthreecharacterslongand

containonlyletters');

}

});

constcheckGameExists=function(id,res,onSuccess,onError){

service.get(id)

.then(game=>{

if(game){

onSuccess(game);

}else{

res.status(404).send('Non-existentgameID');

}

})

.catch(onError);

};

...

router.delete('/:id',function(req,res,next){

checkGameExists(

req.params.id,

res,

game=>{

if(game.setBy===req.user.id){

game.remove()

.then(()=>res.send())

.catch(next);

}else{

res.status(403).send('Youdonothavepermissiontodelete

thisgame');

}

},

next);

});

Thisfilewasthesimplestbefore,soshowstheleastdifferencehere.Westillhavealittlerepetitionofboilerplate(forexample,thecatchcall).Still,thepromise-basedapproachismorecompactandreadablethanwithcallbacks.Nowlet'slookattheindexroutecodefromsrc/routes/index.js:

varexpress=require('express');

varrouter=express.Router();

vargames=require('../service/games.js');

router.get('/',function(req,res,next){

games.createdBy(req.user.id)

.then(gamesCreatedByUser=>

games.availableTo(req.user.id)

.then(gamesAvailableToUser=>{

res.render('index',{

title:'Hangman',

userId:req.user.id,

createdGames:gamesCreatedByUser,

availableGames:gamesAvailableToUser

});

}))

.catch(next);

});

module.exports=router;

Thisisalittlebetter.Thereislessrepetition,butstillsomenestingandboilerplate.Notethattheoutermostthencallbackreturnsapromise(chainedfromgames.availableTo).Whenathencallbackreturnsapromise,thisiseffectivelyflattened,sotheoverallpromisereturnsthevalueoftheinnerpromise.Thisflatteningalsoappliestothepropagationoferrors,sowedon'tneedtocallcatchontheinnerpromiseexplicitly.

Thiscodeisstillalittleconfusingtofollow.Thereisactuallyawaytomakeitmuchmorereadable,whichwe'llcomebacktoshortly.Let'sfirstlookatthebeforeEachfunctioninthegamesservicetestinthefollowingcodefromtest/service/games.js:

describe('Gameservice',function(){

letfirstUserId='user-id-1';

letsecondUserId='user-id-2';

beforeEach(function(done){

service.availableTo('non-existent-user')

.then(games=>games.map(game=>game.remove()))

.then(gamesRemoved=>Promise.all(gamesRemoved))

.then(()=>done(),done);

});

});

Thishasbecomemuchshorterandmorelinear.Let'sbreakdownwhateachlinedoes:

service.availableToreturnsapromiseofanarrayofgamesThefirstthencallbackusesarray.maptoconvertthisintoapromiseofanarrayofpromisesofdeleteoperationsThenextthencallbackusesPromise.alltoconvertthisintoasinglepromiseforthewholearrayofdeleteoperations

Note

ThePromise.allfunctiontakesanarrayofpromisesandreturnsapromisethatresolveswhenallofthepromisesinthearrayhaveresolvedorisrejectedassoonasanypromiseinthearrayisrejected.

ThefinalthencallbackisinvokedwhenthepromisereturnedfromPromise.allresolves,thatis,whenallthedeleteoperationsarecomplete,andinvokesMocha'sdonecallback

Notethatunlikewiththecallback-basedapproach,itisalsotrivialtoimplementerrorhandling.Wejustpassinthedonecallbackastheerrorhandler(secondargument)tothefinalthencall.Wecantakeasimilarapproachintheteststhemselvesaswe'vedoneherewiththebeforeEachcallback.Again,theupdatestothetestsareomittedforbrevity,butyoucanfindtheminthebook'scompanioncode.

ParallelisingoperationsusingpromisesWecanalsomakeuseofthePromise.allfunctiontosimplifytheindexroute.Recallthatourcodeisinvokingthetwoasynchronousoperationsoneaftertheother.Inthecallback-basedapproach,attemptingtoexecutetheseinparallelwouldhavemadethecodeevenmorecomplicated.Withpromises,itactuallymakesourcodemorereadable:

varexpress=require('express');

varrouter=express.Router();

vargames=require('../service/games.js');

router.get('/',function(req,res,next){

Promise.all([

games.createdBy(req.user.id),

games.availableTo(req.user.id)

])

.then(results=>{

res.render('index',{

title:'Hangman',

userId:req.user.id,

createdGames:results[0],

availableGames:results[1]

});

})

.catch(next);

});

module.exports=router;

Thisisshorterandmucheasiertounderstand.Wekickofftwoasynchronousoperationstoloaddata,thenmakeuseofthedataassoonasbothoperationshavecompleted.

Tip

Theonlyslightdrawbackoftheprecedingapproachisthatwehavetogeteachofthetwovaluesbackoutofthearraybytheirindex.InNode.jsv6orhigher,wecouldavoidthisandmakethecodemorereadablestillbyusingdestructuringtoassigntwonamedparametersfromthevaluesinthearray,asfollows:

.then(([created,available])=>{...

Thisisn'tusedintheexampleaboveforback-compatibilitywithNode.jsv4.WewilldiscussdestructuringinmoredetailinChapter14,Node.jsandBeyond.

CombiningasynchronousprogrammingpatternsPromisesallowustoaddresssomeoftheshortcomingsofthecallbackpatternandwritemorereadablecode.Nowwehaveanewproblem,though.OneofthemeritsofNode.jsistheconsistentapproachtoasynchronousprogramming.Weseemtohavenegatedthisbyintroducingpromisesaswellastheconventionalcallbackpattern.

Furthermore,althoughnativepromisesarenewtoECMAScript2015,theconceptisnotnew.Therearemanypre-existinglibrariesthatprovidetheirownimplementationofpromises.

Fortunately,thesecompetingapproachestoasynchronousprogrammingareactuallyveryconsistent.ThebiggestvalueoftheconsistencyintheNode.js-stylecallbackpatterncomesfromthefollowing:

Alllibraryfunctionsareasynchronous(non-blocking)bydefaultAllasynchronousoperationsreturnasinglevalueoranerror

Promisesarecompletelyconsistentwiththeabovepoints.ThereisalsoexcellentcompatibilitybetweendifferentimplementationsofpromisesinJavaScript.ThisisthankstothePromises/A+specification(http://promisesaplus.com).Thisessentiallydefinesthebehaviorofthethenmethod.Anypromiselibraryyouarelikelytocomeacrosswillfollowthisspec.NativeJavaScriptpromisesarealsodesignedtobecompatiblewithit.ThesemeansthatalloftheselibrariesandnativeJavaScriptpromisesareinteroperable.

Soalllibrariesusingcallbacksfollowthesameconventionandallpromiselibrariesfollowthesamespecification.Theonlyissueremainingisconvertingbetweenpromisesandcallbacks.Thereareseveralpromiselibrariesthatcandothisforus.

Ifyoujustwanttoconvertafewstandardcallbackfunctionstopromises,youcanusedenodeify,whichcanbeinstalledusingnpm.Ourfs.statexamplefromearlierwouldlooklikethis:

constdenodeify=require('denodeify');

conststat=denodeify(require('fs').stat));

stat('/hello/world')

.then(stats=>console.log('Filelastupdatedat:'+stats.mtime));

Youwillalsofindthatmanylibrariesexposefunctionsthatcanreturnapromiseoracceptacallbackandsocanbeinvokedwitheitherpattern.

SummaryInthischapter,wehaveseenhowtoexposethestandardNode.jscallbackinterfaceinourownmodules.Wehavemadeuseofpromisestoproducemorereadableasynchronouscode.Finally,wehaveseenhowwecanusepromisestogetherwithstandardNode.jscallbacks.

NowthatwecanimplementourownasynchronousAPIs,wecanexpandonourapplicationandstartmakinguseofotherlibrariesthatprovideasynchronousinterfaces.Inthenextchapter,wewillmakeuseofthistointroducepersistentstoragetoourapplication.

Chapter9.PersistingDataMostapplicationsneedtopersistsomekindofdata.Inthischapter,we'llbelookingatsomeapproachestodatapersistenceforNode.jsapplications.

Thedefaultchoiceforpersistenceforalongtimehasbeenthetraditionalrelationaldatabase.YoumayhaveusedRDBMSs(relationaldatabasemanagementsystems)suchasMicrosoftSQLServer,Oracle,MySQLorPostgreSQL.ThesesystemsareoftencategorizedasSQLdatabasessincetheyalluseSQLastheirprimaryquerylanguage.

Morerecently,therehasbeenaproliferationofso-calledNoSQLdatabases.Thisumbrellatermisn'tparticularlyusefulasacategory.SomeNoSQLdatabaseshavenomoreincommonwitheachotherthanwithtraditionalrelationaldatabases.

What'sinterestingistherangeofdatabasesavailableandtheusecasestheyfulfil.TraditionalRDBMSsareaspowerfulandflexibleaseverandtherightchoiceformanysituations.Inthischapter,we'llconsidertwoothertypesofdatabase,alongwithhowandwhentomakeuseofthem.

Thesystemswe'llbelookingatareMongoDBandRedis.Bothofthesehadtheirinitialreleasein2009andarenowwidely-used.Coveringeitherofthemindepthwouldjustifyabookinitself.Theaimofthischapteristoprovideanintroductiontoandhigh-leveloverviewofeach.

Inthischapter,wewillcoverthefollowingtopics:

TheconceptualdatamodelusedbyeachofthesesystemsTheusecasesforwhichtheyprovidethemostbenefitIntegratingthemwithanExpressapplicationTestingdatapersistencecode

IntroducingMongoDBMongoDBisadocument-orientedDBMS.MongoDBdocumentsarestoredasbinaryJSON(BSON).ThisissimilartoJSON,butwithsupportforadditionaldatatypes.JSONfieldvaluescanonlybestrings,numbers,objects,arrays,Booleans,ornull.BSONsupportsmorespecificnumerictypes,datesandtimestamps,regularexpressions,andbinarydata.Asthenamesuggests,BSONdocumentsarestoredandtransferredasbinarydata.ThiscanbemoreefficientthanJSON'sstringrepresentation.

MongoDBdocumentsarestoredincollections.Theseworkverymuchliketablesinatraditionalrelationaldatabase.Documentscanbeinserted,updated,andqueried.Therearetwokeydifferencesfromatraditionalrelationaldatabase:

MongoDBdoesnotsupportserver-sidejoins.InatraditionalRDBMS,youwouldnormalizedataintomultipletablesandjoinacrossthemusingforeignkeys.InMongoDB,youinsteaduseBSON'snestedstructuretodenormalizedataabouteachentityintoasingledocument.Therelationalpropertyofarelationaldatabaseisthatallrowsinatablecontainthesamefieldswiththesamemeaning.InMongoDB,documentscanhaveanysetoffields.

Inpractice,documentsinthesamecollectiontypicallyhavethesamefieldsoratleastacommoncoresetoffields.MongoDBsupportsthecreationofindexesoncommonfieldsinacollectiontomakequeryingmoreefficient.

WhychooseMongoDB?ThereareseveralpropertiesofMongoDBthatmakeitanappealingchoiceforsomeusecases,especiallyinNode.js-basedapplications.We'llcovertheseinthissection.

Objectmodeling

MongoDB'sdocument-basedapproachcanbeagoodfitforpersistingdomainentities.YoumayhaveexperienceofstoringdomainentitiesinarelationaldatabaseusinganObject-RelationalMapper(ORM).HibernateandEntityFrameworkarepopularexamplesofORMs.OneofthejobsperformedbyanORMismappingasingleentitytomultipletablesinanormalizedschema.Whenanentityisloadedfromthedatabase,itisreconstructedviaJOINqueriesbetweenthesetables.ThisisoneofthekeyfeaturesofORMs.ItisalsooneofthemostcommonsourcesofconfigurationproblemsandperformanceissueswhenusinganORM.MongoDBpersistseachentityasasingledocument,whichcanbemuchsimpler.

Ofcourse,cross-tablejoinscanalsobeusefulfortraversingrelationshipsbetweenentities.WhileORMstypicallymakethiseasy,thiscanitselfbeasourceofperformanceproblems.ImplicitloadingofrelatedentitiesoftencausesN+1problems,issuingthousandsofDBqueries.Handlingtheserelationshipswellrequirescarefulthought,whateverkindofdatabaseyouareusing.

WhenusinganORMandanRDBMS,allinter-entityrelationshipsareforeignkeys,butyouneedtothinkcarefullyabouthowtoloadthem.WhenmodelingdatainMongoDB,youmustchoosebetweenembeddeddocumentsordocumentreferencesforinter-entityrelationships.Undereithertechstack,thedesigndecisionsdependonthedataaccessrequirementsofyourapplicationanddesigningthedatamodeltoreducetheprevalenceofinter-entityrelationshipswillsimplifymatters.

JavaScript

MongoDBisagoodfitforNode.jsinparticular.TheuseofaJSON-likeformatmapswelltoaJavaScript-basedprogrammingenvironment.MongoDBitselfalsorunsJavaScriptnatively.DatabaseoperationscanmakeuseofcustomJavaScriptfunctionsthatexecuteontheserver.

Scalability

MongoDBalsoscalesinasimilarmannertoNode.js.Itusespartitioningandreplicationtosupporthorizontalscalingoncommodityhardware.Thereisnotechnicalreasonwhyyourapplicationanddatabasehavetoscaleinthesameway,butitmaybeeasiertoplanforscalabilityfromabusinessperspective.

WhenusinganRDBMS,itismorestraightforwardtoscalethedatabasevertically.Thatmeansprovisioningahigh-powereddatabaseserverthatcansupportmultipleapplicationservers.Thisrequiresmorecarefulplanningandmoreup-frontinvestmentthanlinearlyscalingapplicationanddatabaseservershorizontallytogether.

GettingstartedwithMongoDBVisithttps://www.mongodb.org/downloadstodownloadandinstallthelatestversionoftheMongoDBCommunityServereditionforyouroperatingsystem.Therearemoredetailedinstallationinstructionsintheusermanualathttps://docs.mongodb.org/manual/installation/.

ThecommandsintherestofthissectionmakeuseofexecutablesinMongoDB's/bindirectory.Youcanrunthecommandsinthisdirectoryor,betterstill,addittoyourPATH.

CreateadirectoryforMongoDBtostoreitsdata.ThenstarttheMongoDBdaemonprocess(thatis,service),providingthepathofthatdirectoryasfollows:

>mongod--dbpathC:\data\mongodb

UsingtheMongoDBshell

YoucaninteractwithMongoDBfromtheconsoleusingitsbuilt-inshellapplication.YoucanlaunchtheMongoDBshellbyrunningthemongocommand,asfollows:

>mongodemo

Thiswillconnecttoadatabasenameddemo(creatingit,ifnecessary)onthelocalserver.Ifyoudon'tspecifyadatabase,thentheshellconnectstoadatabasenamedtest.

ThefirstthingtonoticeisthattheshellisjustanotherJavaScriptenvironment.WecantryrunningsomeofthesamecommandsasatthebeginningofChapter2,GettingStartedwithNode.js.

>functionsquare(x){returnx*x;}

>square(42)

1764

>newDate()

ISODate("2016-01-01T20:05:39.652Z")

>varfoo={bar:"baz"}

>typeoffoo

object

>foo.bar

baz

JustasNode.jsbuildsonJavaScriptinwaysthatmakeitmoresuitableforserver-sideapplicationdevelopment,MongoDBaddsfeaturesmoreusefultodatapersistence.NotethatnewDate()intheprecedingcodereturnsanISODate,MongoDB'sstandarddatatypeforrepresentingdatesinBSONdocuments.

Youcanquittheconsolebytypingexitatanytime.

MongoDBalsoaddssomenewglobalvariablesforinteractingwiththedatabase.Themostimportantoftheseisthedbobject.Let'stryaddingsomedocumentstoourdatabase.RecallthatMongoDBstoresdocumentsincollections.Tocreateanewcollection,wejustneedtostartinsertingdocumentsintoit.Forasimpleexample,we'llusetheUKbankholidaysfor2016.We

canpopulatethiscollectionusingthefollowingscript:

db.holidays.insert(

{name:"NewYear'sDay",date:ISODate("2016-01-01")});

db.holidays.insert(

{name:"GoodFriday",date:ISODate("2016-03-25")});

db.holidays.insert(

{name:"EasterMonday",date:ISODate("2016-03-28")});

db.holidays.insert(

{name:"EarlyMaybankholiday",date:ISODate("2016-05-02")});

db.holidays.insert(

{name:"Springbankholiday",date:ISODate("2016-05-30")});

db.holidays.insert(

{name:"Summerbankholiday",date:ISODate("2016-08-29")});

db.holidays.insert(

{name:"BoxingDay",date:ISODate("2016-12-26")});

db.holidays.insert(

{name:"ChristmasDay",date:ISODate("2016-12-27"),

substitute_for:ISODate("2016-12-25")});

NotethatChristmasDayfallsonaSundayin2016,sothebankholidayoccursonthenextworkingday.Thisgivesusareasontohaveanotherfieldthatisonlyrelevanttosomedocumentsinthecollection.

Youcouldtypetheseinsertcommandsintotheconsolemanually,butit'seasiertotellMongoDBtoloadthemfromascriptfile:

>mongodemoholidays.js--shell

Thepreviouscommandconnectstoadatabasenameddemo,runstheholiday.jsscript(availableinthebook'scompanioncode),thenopensashelltoallowustointeractwiththedatabase.WecanviewthecompletecontentsofthecollectionbyrunningthefollowingcommandintheMongoDBconsole:

>db.holidays.find()

{"_id":ObjectId("572f760fffb6888d70c45eeb"),"name":"NewYear'sDay",

"date":ISODate("2016-01-01T00:00:00Z")}

{"_id":ObjectId("572f7610ffb6888d70c45eec"),"name":"GoodFriday",

"date":ISODate("2016-03-25T00:00:00Z")}

...

NotethatMongoDBhasautomaticallyaddedan_idfieldtoeachdocumentforus.

Tip

YoucanseehowMongoDBdoesthisbyviewingthesourceoftheinsertmethod.Justtypedb.holidays.insertintotheshell(withnoparentheses).

Wecanpulloutrecordsbytheir_idorothersinglefields:

>db.holidays.find({name:"BoxingDay"})

Thiswillreturnanyobjectsthatmatchtheobjectpassedtofind.Tolookupdocumentsbysomethingotherthanexactequality,wecanuseMongoDB'squeryoperators.Theseareprefixedwiththedollarsymbolandspecifiedasobjectproperties.Forexample,tofindholidaysinthesecondhalfoftheyear,wecanusethegreaterthanorequaltooperatorasfollows:

>db.holidays.find({date:{$gte:newDate("2016-07-01")}})

MongoDB'saggregationpipelineallowsustobuildcomplexqueriesfromasequenceofoperationscalledpipelinestages.ItistheclosestthinginMongoDBtocomplexqueryinginSQL.Here,wecountthenumberofbankholidaysineachmonthusingMongoDB's$grouppipelinestage,whichissimilartoSQL'sGROUPBYclause:

>db.holidays.aggregate({

$group:{_id:{$month:"$date"},count:{$sum:1}}})

Anoddquirkofthecalendarin2016meansthattheChristmasDayBankHolidayactuallycomesafterBoxingDay(sinceChristmasDayitselfisonaSunday).Inthefollowingexample,weorderbankholidaysbythedateoftheoccasionthattheymark(storedinthe$substitute_forfieldifdifferentfromthedateofthebankholiday):

>db.holidays.aggregate([

{$project:{_id:false,name:"$name",

date:{$ifNull:["$substitute_for","$date"]}}},

{$sort:{date:1}}

])

Thepreviouspipelineconsistsoftwostages:

The$projectstagespecifiesasetoffieldsbasedontheunderlyingdata(similartoSELECTinSQL).Notethatthe_idfieldisincludedbydefault,butweexcludeithere.The$sortstagespecifiesasortfieldanddirection(similartoSQL'sSORTBYclause).The1hereindicatesanascendingsortorder.

Wehavejustscratchedthesurfacehere.TherearemanymorepipelinephasesavailableinMongoDB.YoucanfindoutmoreaboutaggregationintheMongoDBdocumentationathttps://docs.mongodb.com/manual/core/aggregation-pipeline/.

MongoDBalsohasabuilt-inMap-ReducefunctionforpowerfulaggregatedataprocessingusingarbitraryJavaScriptfunctions.Thisisbeyondthescopeofthisbook,butyoucanfindoutmoreaboutMap-ReduceandMongoDB'simplementationofitathttps://docs.mongodb.com/manual/core/map-reduce/.

UsingMongoDBwithExpressThegamesservicemoduleinourapplicationcurrentlystoresallitsdatainmemory.Thisworkedwellenoughfordemopurposes,butisn'tsuitableforarealapplication.Weloseallthedatawhenevertheapplicationrestarts.Italsopreventsusfromscalingourapplicationacrossmultipleprocesses.Eachinstancewouldhaveitsowngameservicewithdifferentdata.Userswouldseedifferentdatadependingonwhichserverhappenedtohandletheirrequest.

We'regoingtoupdateourgamesservicetostoreitsdatainMongoDB.Forthis,we'regoingtomakeuseofalibrarycalledMongoose.

PersistingobjectswithMongooseRecallthat,unlikearelationaldatabase,MongoDBdoesnotrequiredocumentsinthesamecollectiontohavethesamefields.However,wedotypicallyexpectmostitemswithinacollectiontoshareatleastacommoncoreoffields.

MongooseisanobjectmodelinglibraryforstoringentitiesinMongoDB.Ithelpswithwritingcommonfunctionalitysuchasvalidation,querybuilding,andtypecasting.Italsoprovideshooksforassociatingbusinesslogicwithourentities.ThesearesimilartosomeofthefeaturesprovidedbyORMssuchasEntityFrameworkorHibernate.MongooseitselfisnotanORM,though.Recallthatobject-relationalmappingisnotrelevantfordocument-orienteddatabasessuchasMongoDB.

TouseMongoose,westartbydefiningaschema.ThisdefinesthecommonfieldsfordocumentswithinaMongoDBcollection.Returningtoourdemoapplicationfromtheprecedingchapters,let'sinstallMongooseanddefineaschemaforourgamescollection:

>npminstallmongoose--save

Thefollowingcodeisaddedtosrc/services/games.js:

'usestrict';

constmongoose=require('mongoose');

constSchema=mongoose.Schema;

constgameSchema=newSchema({

word:String,

setBy:String

});

Theschemadefinesdocumentfieldsandspecifiesthetypeofeachfield.Tostartpersistingdocumentswiththisschema,weneedtocreateamodel.

ModelsareconstructorsthatcorrespondtoaMongoDBcollection.InstancesofaMongoosemodelcorrespondtodocumentsinthatcollection.Modelsalsoprovidefunctionsformodifyingthecollection.Wecreateamodelbyspecifyingtheschemaand(singular)collectionname:

constgameSchema=newSchema({

word:String,

setBy:String

});

constGame=mongoose.model('Game',gameSchema);

TheModelconstructorreplacesourGameclassandconstructorfrombefore.Thisclassalsocontainedtwoinstancemethods:positionsOfandremove.Wecandefinecustominstancemethodsonaschema,whichwillbeavailableonallmodelinstances.Thesemustbedefinedbeforecreatingthemodel:

constgameSchema=newSchema({

word:String,

setBy:String

});

gameSchema.methods.positionsOf=function(character){

letpositions=[];

for(letiinthis.word){

if(this.word[i]===character.toUpperCase()){

positions.push(i);

}

}

returnpositions;

};

constGame=mongoose.model('Game',gameSchema);

Note

Notethatweuseatraditionalfunctiondefinitionratherthananarrowfunctionintheprecedingcode.Thisisnecessaryinorderforthethiskeywordinsidethefunctiontoworkcorrectly.Seehttp://derickbailey.com/2015/09/28/do-es6-arrow-functions-really-solve-this-in-javascript/formoredetails.

Wedon'tneedtodefinearemovemethodanymore,becauseMongooseprovidesthisautomatically.Italsoprovidesasavemethod,whichwecanuseforpersistingnewgames:

constGame=mongoose.model('Game',gameSchema);

module.exports.create=(userId,word)=>{

letgame=newGame({setBy:userId,word:word.toUpperCase()});

returngame.save();

};

Wedon'tneedtospecifyanIDanymore,sincethisisalsoprovidedbyMongoose.Notethatwedoneedtospecifyword.toUpperCase(),whichusedtobeintheGameconstructor.Thisisn'taproblem,sincetheconstructorisprivatetoourmodule.Nocodeoutsidethemodulecaninvoketheconstructordirectly.WherethetoUpperCasecalltakesplaceisjustanimplementationdetail.

AlsonotethatMongoose'sasyncoperationsallreturnpromisesasanalternativetousingcallbacks.Mongoosesupportsbothoftheasynchronousprogrammingpatternsdiscussedinthepreviouschapter.Mongooseusesitsownimplementationofpromises.WecanconfigureMongoosetouseECMAScript6promises,though.WealsoneedtotellMongoosetoconnecttoaMongoDBdatabase.Fornow,wewillhardcodetheURL,butwe'llseehowtomakethisconfigurableshortly:

constmongoose=require('mongoose');

mongoose.Promise=Promise;

mongoose.connect('mongodb://localhost/hangman');

Finally,weneedtoimplementourthreemethodsforretrievinggamesfromthedatabase.Wecan

dothisusingMongoose'sfindmethod:

module.exports.create=(userId,word)=>{

...

};

module.exports.createdBy=

(userId)=>Game.find({setBy:userId});

module.exports.availableTo=

(userId)=>Game.find({setBy:{$ne:userId}});

module.exports.get=

(id)=>Game.findById(id);

TheMongoosefindmethodworksliketheMongoDBfindmethodwesawintheprevioussection,UsingtheMongoDBshell.IttakesasetofMongoDBqueryconditionsandasynchronouslyprovidesalistofdocuments.findByIdtakesanIDandasynchronouslyprovidesasingledocument,ornull.

Mongoosealsoprovidesawheremethodforbuildingupconditionsthroughfunctioncalls.TheavailableTofunctioncanberewrittenasfollows:

module.exports.availableTo=

(userId)=>Game.where('setBy').ne(userId);

AslongasyoustillhaveMongoDBrunninglocally(asdescribedinGettingstartedwithMongoDBearlierinthechapter),youshouldnowbeabletoruntheapplication.Trystoppingandrestartingtheapplicationandnoticethatgamesarenowpersistedbetweenrestarts.

IsolatingpersistencecodeIt'susefultointegratewitharealdatabasetomakesureourpersistencecodeisworking.Butit'snotalwaysappropriateforourteststobedependentonanexternalMongoDBinstance.

Wewantdeveloperstobeabletocheckoutthecodeandruntheapplicationwithoutneedingtorunadatabaseinstance.Also,externaldependenciesslowdownourtests.MongoDBstoresdataondisk,sointroducesadditionalI/Oworkintoourtests.

Theapplicationshoulddependonanexternaldatabaseinproduction.Inintegration,wewanttousearealdatabaseonthelocalserver.Ondevelopmentmachines,itwouldbebettertouseanin-memorydatabasebydefault.SoweneedtobeabletoconfigureadatabaseURLandfallbacktoanin-memorydatabaseindevelopmentenvironments.

Finally,weneedtoinitializeMongoosebeforeusingitinourgamesservice.ThisincludesspecifyingthedatabaseURLandwaitingforaconnectiontobeestablished.Thishappensasynchronously,socan'tbepartofthegamesservicemoduledefinition.Wealsodon'twantclientsofthegamesservicetohavetopassinaMongooseinstancetoeachfunctioncall.

Wecanaddressalloftheseissuesbyintroducingdependencyinjectiontoourapplication.We'llpassinthegameserviceasadependencytothemodulesthatneeditandpassinMongooseasadependencytothegamesservice.

Tip

Thiswouldalsogiveustheoptionofwritingunittestsforothermodulesthatpassinatestdoubleforthegamesserviceitself,sodon'tuseMongoDBatall.Inlargerapplications,thiskindoftestisolationisimportantforwritingfastandmaintainabletests.

DependencyinjectioninNode.jsYoumayhaveuseddependencyinjection(DI)frameworkssuchasUnity,Autofac,NInject,orSpringin.NETorJava.Theseprovidefeaturessuchasdeclarativeconfigurationandautowiringofdependencies.TherearesimilarDIcontainersavailableforJavaScript.However,itismorecommontopassarounddependenciesexplicitly.JavaScript'smodulepatternmakesthisapproachmorenaturalthaninotherlanguages.Wedon'tneedtoaddalotoffieldsandconstructors/propertiestosetupdependencies.Wecanjustwrapmodulesinaninitializationfunctionthattakesdependenciesasparameters.

Inourapplication,theappmodulewillwireeverythingtogether.Theapplicationasawholedependsonthedatabase.Thegamesandindexroutesdependonthegameservice.Toallowtheroutestotakeadependencyonthegameservice,wejustneedtotopandtailthemwithafunction:

'usestrict';

module.exports=(gamesService)=>{

varexpress=require('express');

varrouter=express.Router();

...

returnrouter;

};

Thegamesserviceitselfisslightlymorecomplicated.Wepreviouslyaddedseveralfunctionstomodule.exports,soweneedtoputtheseonanobjectinstead.However,thisactuallyresultsinshortercode.Also,notethatweonlycreatetheGameschemaifithasn'talreadybeendefined,todefendagainstourexportedfunctionbeingcalledmultipletimes:

module.exports=(mongoose)=>{

'usestrict';

letGame=mongoose.models['Game'];

if(!Game){

constSchema=mongoose.Schema;

constgameSchema=newSchema({

word:String,

setBy:String

});

gameSchema.methods.positionsOf=function(character){

...

};

Game=mongoose.model('Game',gameSchema);

}

return{

create:(userId,word)=>{

constgame=newGame({

setBy:userId,word:word.toUpperCase()

});

returngame.save();

},

createdBy:userId=>Game.find({setBy:userId}),

availableTo:userId=>Game.where('setBy').ne(userId),

get:id=>Game.findById(id)

};

};

Finally,theapplicationitselfdependsonthedatabaseconnectionandwiresuptheotherdependencies:

module.exports=(mongoose)=>{

...

letgamesService=require('./services/games')(mongoose);

letroutes=require('./routes/index')(gamesService);

letgames=require('./routes/games')(gamesService);

...

returnapp;

};

ProvidingdependenciesWecanspecifythedatabaseURLinanenvironmentvariable.Whenthisisn'tpresent,ourapplicationwillinsteadmakeuseofanin-memoryinstanceofMongoDB.ThiswillbeprovidedbyalibrarycalledMockgoose.Weinstallthisasadevdependency,incaseweforgettosetourenvironmentvariableonaproductionserver.We'llgetanerrorratherthanquietlyusinganon-persistentdatabase.

>npminstallmockgoose@~5.x--save-dev

Wecreateanewmoduleundersrc/config/mongoose.jstoinitializeMongooseandreturnapromisethatwillbefulfilledwhenithasconnectedtothedatabase:

'usestrict';

constmongoose=require('mongoose');

constdebug=require('debug')('hangman:config:mongoose');

mongoose.Promise=Promise;

if(!process.env.MONGODB_URL){

debug('MongoDBURLnotfound.Fallingbacktoin-memorydatabase...');

require('mockgoose')(mongoose);

}

letdb=mongoose.connection;

mongoose.connect(process.env.MONGODB_URL);

module.exports=newPromise(function(resolve,reject){

db.once('open',()=>resolve(mongoose));

db.on('error',reject);

});

Nowwejustneedtopassthisintoourapplication.Thefollowingisthecodefrombin/www:

...

require('../src/config/mongoose').then((mongoose)=>{

varapp=require('../src/app')(mongoose);

...

server.on('listening',onListening);

}).catch(function(error){

console.log(error);

process.exit(1);

});

Toallowourteststorun,we'llalsoneedtoaddnewbeforefunctionstomakeuseofthismodule.Thefollowingcodeisfromtest/services/games.js:

'usestrict';

constexpect=require('chai').expect;

describe('Gameservice',()=>{

constfirstUserId='user-id-1';

constsecondUserId='user-id-2';

letservice;

before(done=>{

require('../../src/config/mongoose.js').then((mongoose)=>{

service=require('../../src/services/games.js')(mongoose);

done();

}).catch(done);;

});

...

Thefollowingcodeisfromtest/routes/games.js:

'usestrict';

constrequest=require('supertest');

constexpect=require('chai').expect;

describe('/games',()=>{

letagent,userId;

letmongoose,gamesService,app;

before(function(done){

require('../../src/config/mongoose.js').then((mongoose)=>{

app=require('../../src/app.js')(mongoose);

gamesService=

require('../../src/services/games.js')(mongoose);

done();

}).catch(done);

});

...

We'llalsoaddaglobalteardownfunctiontoclosethedatabaseconnectionafteralltestshavefinished.ThisisjustaMochaafterhookoutsidethecontextofanydescribeblock.Weaddthisinanewfileundertest/global.js:

'usestrict';

after(function(done){

require('../src/config/mongoose.js').then(

(mongoose)=>mongoose.disconnect(done));

});

Finally,weneedtoupdateourgulpfile.js,toallowourintegrationteststorunwiththenewdependency:

gulp.task('integration-test',

['lint-integration-test','test'],function(done){

constTEST_PORT=5000;

require('./src/config/mongoose.js').then((mongoose)=>{

letserver,teardown=(error)=>{

server.close(()=>

mongoose.disconnect(()=>done(error)));

};

server=require('http')

.createServer(require('./src/app.js')(mongoose))

.listen(TEST_PORT,function(){

gulp.src('integration-test/**/*.js')

.pipe(

...

)

.on('error',teardown)

.on('end',teardown)

});

});

});

WecannowrunourapplicationandtestsonalocaldevelopmentmachinewithoutneedingtohaveMongoDBrunning,orwecanspecifytheMONGO_DBenvironmentvariableifandwhenwewanttousearealMongoDBinstance.

RunningdatabaseintegrationtestsonTravisCIWedowanttoregularlyintegrationtestourapplicationagainstarealMongoDBinstance.Fortunately,TravisCIprovidesvariousdatastoresaspartofitsenvironment.WejustneedtotellitthatourbuildrequiresMongoDBbyaddingittoourtravis.ymlfile.WealsoneedtosettheMONGODB_URLenvironmentvariableforteststobeabletoconnecttothedatabase:

services:

-mongodb

env:

global:

-MONGODB_URL=mongodb://localhost/hangman

NowwecanrunourapplicationaswellasourunitandintegrationtestswithasuitableMongoDBinstanceondevelopmentmachinesandontheCIserver.

IntroducingRedisRedisisoftenclassifiedasakey-valuedatastore.Redisdescribesitselfasadata-structurestore.Itoffersstoragetypessimilartothebasicdatastructuresfoundinmostprogramminglanguages.

WhyuseRedis?Redisoperatesentirelyinmemory,allowingittobeveryfast.This,togetherwithitskey-valuenature,makesitwell-suitedforuseasacache.Italsosupportspublish/subscribechannels,whichallowsittofunctionasamessagebroker.We'lllookatthisfurtherinChapter10,Real-timeWebAppsinNode.js.

Moregenerally,RediscanbeausefulbackendtoallowmultipleNode.jsprocessestoco-ordinatewithoneanother.Node.jsscaleshorizontallyandmostwebsiteswillrunmultipleNode.jsprocesses.Manywebsiteshave"working"datathatdoesn'tneedtobepersistedlongterm,butdoesneedtobeavailablequicklyandconsistentlyacrossallprocesses.Redis'sin-memorynatureandrangeofatomicoperationsmakeitveryusefulforthispurpose.

Redisisbuiltmoreforspeedthandurability.Therearevariousoptionstoconfigureit,butallexpectsomeamountofdatalossintheeventofanoutage.ThisisacompromiseofRedisworkingentirelyin-memoryforspeed.Itispossibletoreducedatalosstonomorethanthelastsecondofwritesbeforeanoutage,withoutsignificantlycompromisingonspeed.Rediscanbeconfiguredtocompletelyminimizedatalossbysyncingtodiskaftereachoperation.However,thishasamoresignificantimpactonperformanceandnegatestheadvantagesofRedis'sin-memorynature.

InstallingRedisSourcedistributionsofRedisareavailablefromhttp://redis.io/download.

ForWindows,itismoreusefultodownloadapre-builtbinary.ItisavailableasasignedpackageviaNuGetandChocolatey.IfyouhaveChocolateyavailable,youcaninstallRedisbyrunningthefollowingcommand:

>chocoinstallredis-64

Alternatively,youcandownloadanunsignedversionoftheinstallerfromhttps://github.com/MSOpenTech/redis/releases

Onceinstalled,youcanstartRedisbyrunningredis-server.Inaseparatewindow,runredis-clitoconnecttotheserverandruncommands.

UsingRedisasakey-valuestoreEverythinginRedisisstoredagainstakey.KeysinRediscanbeanybinarydata,butit'sbesttothinkofthemasstrings.Varioustypesofvaluecanbestoredagainsteachkey.

RedisreferstosimplescalarvaluesasStrings.Redisalsohasspecialtreatmentforscalarintegers.Thefollowingexamplesetsandupdatesakeynamedcounter:

127.0.0.1:6379>setcounter100

OK

127.0.0.1:6379>getcounter

"100"

127.0.0.1:6379>incrcounter

(integer)101

Thisincrementoperationisatomic.Redisalsosupportssettingvaluesatomically.Thefollowingcommandwillfailbecausethekeyalreadyexists:

127.0.0.1:6379>setcounter200nx

(nil)

Thesefeaturescanhelpcoordinatingbetweenservers.Redisalsosupportssettingexpirytimesforkeys.Thismakesitpossibletooffercachingbehaviorsimilartomemcache.Redishasevenmoreflexibility,though,aswe'llseeinthenextsection.

StoringstructureddatainRedisInadditiontosimplekey-valuepairs,Redissupportsothermorestructureddatatypes.

Listsareorderedcollectionsofvalues.Theyarestoredasalinkedlistratherthanasarrays.Thismakesadding/removingelementsattheendsofthelistefficient(atthecostofslowerretrievalofitemsfromthelistbyindex),forexample:

127.0.0.1:6379>rpushfruitapplebananapear

(integer)3

127.0.0.1:6379>rpopfruit

"pear"

127.0.0.1:6379>lpushfruitorange

(integer)3

127.0.0.1:6379>lrangefruit0-1

1)"orange"

2)"apple"

3)"banana"

Notethatlrangetakesstartandendindices.Negativevaluescountbackwardsfromtheendofthelist,so-1referstothelastelement.Beingabletopush/popfromeitherendofalistmeansthattheycanbeusedasstacksorqueues,forexample,forallowingprocessestocommunicateinaproducer-consumerarrangement.

Hashesareasetoffield-valuepairs.ThesearenotasrichasMongoDBdocuments,butallowustoassociatesomedatatogether.Forexample,wecouldhaveimplementedourgameserviceusingRedis:

127.0.0.1:6379>hmsetgame:2wordJavaScriptsetByuser-id-7

OK

127.0.0.1:6379>hgetgame:2word

"JavaScript"

127.0.0.1:6379>hgetallgame:2

1)"word"

2)"JavaScript"

3)"setBy"

4)"user-id-7"

Notethatthetop-levelkeygame:2hereisjustaconvention.Itcanbeusefulfordeveloperstonamespacekeysinthisway,butRedisonlyunderstandsthemasstrings.

Setsareunorderedcollectionsofvalues,forexample:

127.0.0.1:6379>saddnumbersonetwothree

(integer)3

127.0.0.1:6379>smembersnumbers

1)"two"

2)"three"

3)"one"

Setssupportmathematicaloperationssuchasunionsandintersections.Theyalsosupportthe

retrieval(withoptionalatomicremoval)ofrandomelements.

Sortedsetsarecollectionsofvalues,eachassociatedwithanumericalscore:

127.0.0.1:6379>zaddvotes3Aye

(integer)1

127.0.0.1:6379>zaddvotes4No

(integer)1

127.0.0.1:6379>zaddvotes1Abstain

(integer)1

127.0.0.1:6379>zrevrangevotes01

1)"No"

2)"Aye"

Notethattherangesareorderedsmallesttolargestbydefault.Werequestareverserangeabovetogettheelementwiththehighestscorefirst.Sortedsetsareusefulforimplementingvotingsystems(aspreviouslyshown)orrankingsystems.

BuildingauserrankingsystemwithRedisWewanttobeabletorankusersbasedonhowmanygamestheyhavecompleted.Wewillcreateauserservice,implementedinRedis,thatprovidesthefollowingfunctionality:

RecordwhenausersuccessfullycompletesagameReturnthetopthreeusersacrossthesiteReturntherankofagivenuser

Wewillfirstaddafeaturetomakethesiteabitmoreuser-friendlybyallowinguserstochooseascreenname.

UsingRedisfromNode.jsFirst,we'llneedtoinstallaNode.jsclientlibraryforRedis.We'llalsousethepromiselibraryBluebirdtoconverttheRedisclientlibrarytopromises:

>npminstallredis--save

>npminstallbluebird--save

First,we'llcreateamoduleforconfiguringtheRedisclientasshownhereinsrc/config/redis.js:

'usestrict';

constbluebird=require('bluebird');

constredis=require('redis');

bluebird.promisifyAll(redis.RedisClient.prototype);

module.exports=redis.createClient(process.env.REDIS_URL);

Nowwecancreateanewuserservicewithmethodsforgettingandsettingausername,insrc/services/users.js:

'usestrict';

letredisClient=require('../config/redis.js');

module.exports={

getUsername:userId=>

redisClient.getAsync(`user:${userId}:name`),

setUsername:(userId,name)=>

redisClient.setAsync(`user:${userId}:name`,name)

};

NotethattheRedisclientprovidesfunctionsforeachRediscommand(suchasgetandset).Bluebirdprovidespromise-basedversionsofeachfunctionsuffixedwithAsync.

Ofcourse,nowthatwehavetestinfrastructureforourproject,weshouldaddtestsfornewcodeaswegoasshownheretest/services/users.js:

'usestrict';

constexpect=require('chai').expect;

constservice=require('../../src/services/users.js');

describe('Userservice',function(){

describe('getUsername',function(){

it('shouldreturnapreviouslysetusername',done=>{

constuserId='user-id-1';

constname='UserName';

service.setUsername(userId,name)

.then(()=>service.getUsername(userId))

.then(actual=>expect(actual).to.equal(name))

.then(()=>done(),done);

});

it('shouldreturnnullifnousernameisset',done=>{

constuserId='user-id-2';

service.getUsername(userId)

.then(name=>expect(name).to.be.null)

.then(()=>done(),done);

});

});

});

Testingwithredis-js

Aswiththetestsforourgamesservice,wewanttobeabletointegratewithaRedisinstanceonourCIserver.Butwedon'twanttointroduceanynewdependenciesfordevelopment.Thistime,wewillmakeuseofalibrarycalledredis-jsforlocaltesting.UnlikeMockgoose,thisdoesnotuseanin-memoryversionoftherealDBengine(Redisisalreadyin-memory).Thisisinsteadare-implementationoftheNode.jsRedisclientthatstoresallofitsdatain-process:

>npminstallredis-js--save-dev

Nowwecancreateamoduleforobtainingtheenvironment-appropriateRedisreferenceasshownheresrc/config/redis.js:

'usestrict';

constbluebird=require('bluebird');

constdebug=require('debug')('hangman:config:redis');

if(process.env.REDIS_URL){

letredis=require('redis');

bluebird.promisifyAll(redis.RedisClient.prototype);

module.exports=redis.createClient(process.env.REDIS_URL);

}else{

debug('RedisURLnotfound.FallingbacktomockDB...');

letredisClient=require('redis-js');

bluebird.promisifyAll(redisClient);

module.exports=redisClient;

}

Notethat,unlikeMongoose,theNode.jsRedisclientcanbeusedimmediately.Anycommandsissuedbeforeithasconnectedareactuallyqueuedupinternally.Thismeanswecanjustreturntheclientfromthemoduleandrequireitdirectly.Therewouldn'tbeanybenefitinthiscasetothedependencyinjectionweusedwithMongoose.

WealsoneedtoaddRedistoour.travis.ymlfilesoitrunsontheCIserver:

services:

-mongodb

-redis-server

env:

global:

-MONGODB_URL=mongodb://localhost/hangman

-REDIS_URL=redis://127.0.0.1:6379/

Finally,weneedtoclosetheclientonceourtestshavecompleted,aswedidwithMongoose.Wealsoensureweemptythedatabaseonstartup(aswedon'thaveawayofdeletinguserdataviatheserviceinterface,aswedowithgames).Thefollowingcodeisfromtest/global.js:

'usestrict';

before(function(done){

require('../src/config/redis.js').flushdbAsync().then(()=>done());

});

after(function(done){

require('../src/config/redis.js').quit();

require('../src/config/mongoose.js').then(

(mongoose)=>mongoose.disconnect(done));

});

Thefollowingcodeisfromgulpfile.js:

letserver,teardown=(error)=>{

require('./src/config/redis.js').quit();

server.close(()=>

mongoose.disconnect(()=>done(error)));

};

ImplementinguserrankingswithRedisNowwearereadytoaddtheuserrankingfunctionalitytoourservice.Thefollowingcodeisfromsrc/services/users.js:

module.exports={

...

recordWin:userId=>

redisClient.zincrbyAsync('user:wins',1,userId),

getTopPlayers:()=>

redisClient.zrevrangeAsync('user:wins',0,2,'withscores')

.then(interleaved=>{

if(interleaved.length===0){

return[];

}

letuserIds=interleaved

.filter((user,index)=>index%2===0)

.map((userId)=>`user:${userId}:name`);

returnredisClient.mgetAsync(userIds)

.then(names=>names.map((username,index)=>({

name:username,

userId:interleaved[index*2],

wins:parseInt(interleaved[index*2+1],10)

})));

}),

getRanking:userId=>{

returnPromise.all([

redisClient.zrevrankAsync('user:wins',userId),

redisClient.zscoreAsync('user:wins',userId)

]).then(out=>{

if(out[0]===null){

returnnull;

}

return{rank:out[0]+1,wins:parseInt(out[1],10)};

});

}

};

MostoftheRediscommandsusedherewillbefamiliarfromearlierinthechapter.ThemostinterestingfunctionisgetTopPlayers.Thismakesuseofzrevrangewiththewithscoresoption.ThisreturnsanarrayofuserIDsandscores(interleavedtogether).Wemakeasecondrequesttothedatabaseusingmget(multivaluedget)toretrievethenamesofalltheusers.Oncethisreturnswecancombineallthedataforeachusertogetherintoanobject.

MakinguseoftheusersserviceWiringthisfunctionalityuptotherestofourapplicationdoesn'tuseanytechniqueswehaven'tseenbefore,soisomittedfromtheprintedcodelistingsforbrevity.Thefullimplementationcanbefoundinthecompanioncodeforthischapter,alongwithtestsfortherestoftheuserservicemethods,athttps://github.com/NodeJsForDevelopers/chapter09.

AnoteonsecurityWehavebeenrunningMongoDBandRediswiththeirdefaultout-of-the-boxsettings.Thisisfinefordevelopmentpurposes.Deployingtheseservicesintoproductionrequiresadditionalconsiderationaroundsecurity.Youcanfindmoreresourcesonthisathttps://docs.mongodb.com/manual/administration/security-checklist/andhttp://redis.io/topics/security.

SummaryInthischapter,wehaveunderstoodthedifferencebetweendifferenttypesofdatabaseandlearnedaboutthekeyfeaturesofMongoDBandRedis.Wealsopersistedourapplication'sdatausingthesedatabasesanduseddependencyinjectiontomakeourapplicationmoreflexible.Wealsolearnedhowtoconfigureourdevelopmentandintegrationenvironmentstouseappropriatedatabaseinstances.

Persistencemaybeconsideredthebottomlayerofoursystem.Inthenextchapter,we'llintroducereal-timeclient/servercommunicationintoourapplication.Thisfrontendfunctionalitymeansfocusingmoreonthetoplayerofoursystem.However,we'llalsoseeRedisplayinganimportantroleinsupportingthisfunctionality.

Chapter10.CreatingReal-timeWebAppsThewebhasofferedanevermoredynamicandinteractiveuserexperience.Throughoutthe90s,mostofthewebconsistedofstaticpagesorserver-siderenderedpages.Framesandiframesmadeitpossibletoreloadpartsofthepageinalimitedway.WhenAjaxappearedinthemid-2000s,itallowedpagestobemuchmoreengaging.Client-sideJavaScriptcouldnowrequestdatafromtheserverondemandandupdatethepagedynamically.

Real-timewebapplicationsarethenextstepinthisevolution.Theseareapplicationswheretheserverpushesdatatoclientswithouttheclientsneedingtoinitiatearequest.Thisallowsausertobenotifiedofnewinformationorforuserstointeractwitheachotherinrealtime.

Inthischapter,wewillcoverthefollowingtopics:

Establishingatwo-waycommunicationchannelbetweentheclientandserverAddingreal-timeinteractivitytoourapplicationIntroducingabackendtoscaleourreal-timeapplicationacrossmultipleservers

Understandingoptionsforreal-timecommunicationReal-timewebapplicationsneedabidirectionalcommunicationchannelbetweentheclientandtheserver.Thisisanypersistentconnectionthatallowstheservertopushadditionaldatatotheclientwhenneeded.TheWebSocketsprotocolisthemodernstandardforthiskindofcommunicationandisimplementedbymostbrowsers.

WebSocketconnectionsareinitiatedviaHTTP,butotherwisedonotdependonit.TheWebSocketprotocoldefinesawayofsendingmessagesbi-directionallyoveraTCPconnection.TCPisthelow-leveltransportprotocolthatusuallyunderliesHTTP.WebSocketsarestillarelativelynewtechnologyandnotfullysupportedbyallclientsandservers.MostmodernwebbrowserstodaydosupportWebSockets.However,intermediateservers(proxies,firewalls,andload-balancers)canpreventWebSocketconnectionsfromworking(eitherthroughlackofsupportorintentionallyblockingnon-HTTPtraffic).Inthesecases,therearealternativewaysofachievingreal-timecommunication.

TheEventSourcestandarddefinesawayforaservertosendeventstoclientsoverHTTPanddefinesaJavaScriptAPIforhandlingtheseevents.Itisnotasefficientorwidely-supportedasWebSockets,butisbettersupportedbysomeolderserversandclients.

Theultimatefallbackislong-polling.Thisiswhentheclientinitiatesanordinary(Ajax)requesttotheserver,whichstaysopenuntiltheserverhassomedatatosend.Assoonastheclientreceivesanydata,itmakesanotherrequesttotheserverforthenextmessage.ThisintroducesadditionalbandwidthoverheadsandlatencycomparedtoWebSockets,buthasthewidestsupportasitjustusesordinaryHTTPrequests.

Ideally,aclientandservercannegotiatetoworkoutthebestavailabletypeofconnectiontouse.Thiscanbequiteacomplicatedprocess,though.Fortunately,therearelibrarieswhichcanhandlethisforus.

IntroducingSocket.IOSocket.IOisamatureandwell-establishedlibrarywithexcellentcross-browsersupport.Itaimstoquicklyandreliablyestablishabidirectionalcommunicationchannelinacross-browsercompatibleway.Itprovidesaconsistentabstraction,basedonidiomaticJavaScriptevents,forreal-timecommunicationbetweentheclientandtheserveroverthischannel.IfyouhaveeverusedSignalRin.NET,youcanthinkofSocket.IOastheJavaScriptequivalent.

ImplementingachatroomwithSocket.IOLet'simplementachatlobbyforusersofourapplicationtotalktooneanother.First,weneedtoinstallSocket.IO:

>npminstall--savesocket.io

Theserver-sideimplementationforthisisverysimple.WejustneedtotellSocket.IOthat,wheneverausersendsachatmessage,wewanttobroadcastthistoallconnectedusersasgivenheresrc/realtime/chat.js:

'usestrict';

module.exports=io=>{

io.on('connection',(socket)=>{

socket.on('chatMessage',(message)=>{

io.emit('chatMessage',message);

});

});

};

Here,weaddalistenertoSocket.IO'sconnectionevent.Ourlistenerisfiredwheneveranewclientconnectstotheapplication.Thesocketvariablerepresentstheconnectiontothatspecificclient.

TheioparametershownpreviouslywillbeaSocket.IOinstance.Tocreateoneofthese,weneedtoprovideareferencetotheHTTPserverthatwillhostourapplication,sothatSocket.IOcanadditsownconnectionhandling.Tokeepthingstidier,we'lladdanewservermoduleinsrc/server.jstosetupourserver,startourExpressapplication,andinitializeSocket.IO:

'usestrict';

module.exports=require('./config/mongoose').then(mongoose=>{

constapp=require('../src/app')(mongoose);

constserver=require('http').createServer(app);

constio=require('socket.io')(server);

require('./realtime/chat')(io);

server.on('close',()=>{

require('../src/config/redis.js').quit();

mongoose.disconnect();

});

returnserver;

});

Thisalsoallowsustosimplifythebootstrapscriptandourintegrationtestsasinbin/www:

#!/usr/bin/envnode

vardebug=require('debug')('hangman:server');

varport=normalizePort(process.env.PORT||'3000');

require('../src/server').then((server)=>{

server.listen(port);

server.on('error',onError);

server.on('listening',onListening.bind(server));

}).catch(function(error){

debug(error);

process.exit(1);

});

...

functiononListening(){

varaddr=this.address();

...

}

...andingulpfile.js:

gulp.task('integration-test',

['lint-integration-test','test'],done=>{

constTEST_PORT=5000;

require('./src/server.js').then((server)=>{

server.listen(TEST_PORT);

server.on('listening',()=>{

gulp.src('integration-test/**/*.js')

.pipe(

...

}))

.on('error',error=>server.close(()=>done(error)))

.on('end',()=>server.close(done))

});

});

});

Nowweneedtoaddtheclient-sidecodetocommunicatewiththisservice.First,we'lladdaplaceforourchatlobbytotheapplicationhomepageasgivenheresrc/views/index.hjs:

{{/topPlayers}}

</ol>

<hr/>

<h3>Lobby</h3>

<formclass="chat">

<divid="messages"></div>

<inputid="message"/><inputtype="submit"value="Send"/>

</form>

</body>

</html>

Now,we'llcreatetheclient-sidescripttoconnectthiswiththeserverasgivenheresrc/public/scripts/chat.js:

$(document).ready(function(){

'usestrict';

varsocket=io();

$('form.chat').submit(function(event){

socket.emit('chatMessage',$('#message').val());

$('#message').val('');

event.preventDefault();

});

socket.on('chatMessage',function(message){

$('#messages').append($('<p>').text(message));

});

});

Finally,weneedtoincludeournewscriptinthepageandincludetheSocket.IOclient-sidescriptthatdefinestheprecedingiofunctionsrc/view/index.hjs:

<!DOCTYPEhtml>

<html>

<head>

<title>{{title}}</title>

<linkrel="stylesheet"href="/stylesheets/style.css"/>

...

<scriptsrc="/scripts/index.js"></script>

<scriptsrc="/socket.io/socket.io.js"></script>

<scriptsrc="/scripts/chat.js"></script>

</head>

<body>

...

Notethatwehaven'tcreatedthesocket.io.jsscriptanywhere.ThisisservedasaresultofattachingSocket.IOtoourserverinsrc/server.js.Sincewedon'tdefinetheiovariableinourownscript,weneedtoletESLintknowthatitexistsasaglobalvariableasgiveningulpfile.js:

gulp.task('lint-client',function(){

returngulp.src('src/public/**/*.js')

.pipe(eslint({envs:['browser','jquery'],

globals:{io:false}}))

.pipe(eslint.format())

.pipe(eslint.failAfterError());

});

Now,ifweopenupourapplicationintwobrowserwindows,theycansendchatmessagestoeachother!

Scalingreal-timeNode.jsapplicationsSinceourchatmessagesarebeingrelayedviatheserver,clientscancurrentlyonlycommunicatewithotherclientsconnectedtothesameserver.Thisisaproblemifwewanttoscaleourapplicationhorizontallyacrossmanyservers.

Thisiseasytofix,buttrickytodemonstrate.Todoso,weneedtohavetwoseparateinstancesofourapplicationrunning.Thiswillbemorerealisticandmoreusefuliftheyarealsousingthesameshareddatabasesforpersistence.SoweneedtostartupMongoDBandRedis,thenstarttwoinstancesofourapplicationondifferentports(sothattheydon'tcollide).

Thismeansrunningallofthefollowingcommands(replacingthedbpathofMongoDBasappropriateforyoursetup):

>redis-server

>mongod--dbpathC:\data\mongodb

>setMONGODB_URL=mongodb://localhost/hangman

>setREDIS_URL=redis://127.0.0.1:6379/

>setPORT=3000

>npmstart

>setPORT=3001

>npmstart

Thecommandsthatstartthedatabaseorapplicationserversalsooccupythecurrentconsole.So,tobeabletorunallofthesecommands,weneedtoexecutetheminseparatewindowsortellthemtoexecuteinthebackground.OnWindows,thiscanbeachievedwiththefollowingbatchscript:

@echooff

START/Bredis-server

START/Bmongod--dbpathC:\data\mongodb

setMONGODB_URL=mongodb://localhost/hangman

setREDIS_URL=redis://127.0.0.1:6379/

SLEEP2

setPORT=3000

START/Bnpmstart

SLEEP1

setPORT=3001

START/Bnpmstart

Nowyoucanconnectseparatebrowserstoaseparateapplicationinstanceathttp://localhost:3000andhttp://localhost:3001.Noticethattwoclientsconnectedtothesameapplicationinstancecanreceivemessagesfromeachother,butnotfromclientsontheotherapplicationinstance.

Toresolvethis,weneedasharedbackendthroughwhichalltheapplicationscancommunicate.Redisisaperfectcandidateforthis.

UsingRedisasabackendSocket.IOmakesuseoftheadapterpatterntosupportdifferentbackends.Anadapterisjustawrapperforconvertingoneinterfaceintoanother.Socket.IOhasastandardbackendinterfaceandvariousadapterstoallowdifferentimplementationstoworkwiththisinterface.Bydefault,itusesanin-memoryadapterthatislimitedtoasingleprocess.However,theSocket.IOprojectalsoprovidesanadaptorforusingRedisasabackend:

>npminstallsocket.io-redis--save

Onceinstalled,usingthisissimplyamatteroftellingSocket.IOwheretofindourRedisinstance(weskipthisintestenvironmentswhereweonlyhaveoneapplicationprocess)asgivenheresrc/server.js:

'usestrict';

module.exports=require('./config/mongoose').then(mongoose=>{

constapp=require('../src/app')(mongoose);

constserver=require('http').createServer(app);

constio=require('socket.io')(server);

if(process.env.REDIS_URL&&process.env.NODE_ENV!=='test'){

constredisAdapter=require('socket.io-redis');

io.adapter(redisAdapter(process.env.REDIS_URL));

}

require('./realtime/chat')(io);

...

returnserver;

});

Andthat'sit!Wedon'trequireanyotherchangestoourcodetosupportscalability.Ifyourestartyourapplicationinstancesnow,youshouldfindthatclientscancommunicatebetweenthem.

IntegratingSocket.IOwithExpressSofar,apartfromsharingthesameserver,theSocket.IOandExpresspartsofourapplicationarecompletelyindependent.Whileit'sgoodthattheyarelooselycoupled,somecross-cuttingconcernsmayberelevanttoboth.

Forexample,bothpartsofourapplicationshouldhaveamutuallyconsistentwayofidentifyingthecurrentuser.Thisisespeciallyimportantiftheyaretocometogethertoprovideasinglecoherentuserexperience.

First,let'sextendourusermiddlewaretoprovidethecurrentuser'snameaswellastheirID,bylookingthemupintheuserserviceasgivenheresrc/middleware/users.js:

'usestrict';

module.exports=(service)=>{

constuuid=require('uuid');

returnfunction(req,res,next){

letuserId=req.cookies.userId;

if(!userId){

userId=uuid.v4();

res.cookie('userId',userId);

req.user={

id:userId

};

next();

}else{

service.getUsername(userId).then(username=>{

req.user={

id:userId,

name:username

};

next();

});

}

};

};

Tip

Youcanfindupdatedtestsforthismiddlewareinthebook'scompanioncode.

Thiswillmeaninjectingouruserserviceasadependency,likewedofortheothermiddlewaremodules(thatis,routes)inourapplicationasgiveninsrc/app.js:

...

letgamesService=require('./service/games')(mongoose);

letusersService=require('./service/users');

letusers=require('./middleware/users')(usersService);

letroutes=require('./routes/index')(gamesService,usersService);

letgames=require('./routes/games')(gamesService,usersService);

letprofile=require('./routes/profile')(usersService);

...

TheinterestingpartisallowingSocket.IOtomakeuseofthismiddleware.Socket.IOhasitsownconceptofmiddlewareverysimilartothatofExpress.RecallthatExpressmiddlewarefunctionstakeparametersforthecurrentrequest,response,andanextcallback.Socket.IOmiddlewarefunctionsjusttakeacommunicationsocketandanextcallback.However,wecanaccesstheoriginalHTTPhandshakethatinitiatedthesocket.ThisallowsustoadaptourExpressmiddlewaretoSocket.IOmiddlewareanduseitasfollows,insrc/server.js:

'usestrict';

module.exports=require('./config/mongoose').then(mongoose=>{

letapp=require('../src/app')(mongoose);

letserver=require('http').createServer(app);

letio=require('socket.io')(server);

if(process.env.REDIS_URL){

letredisAdapter=require('socket.io-redis');

io.adapter(redisAdapter(process.env.REDIS_URL));

}

io.use(adapt(require('cookie-parser')()));

constusersService=require('./services/users.js');

io.use(adapt(require('./middleware/users')(usersService)));

require('./realtime/chat')(io);

...

returnserver;

});

functionadapt(expressMiddleware){

return(socket,next)=>{

expressMiddleware(socket.request,socket.request.res,next);

};

}

NowtheusermiddlewarewillrunforSocket.IOaswellasregularHTTPrequests,makinguserdataavailabletoSocket.IOaswell.Let'susethistoincludeusernamesinourchat.First,weneedtoupdateourserverasgiveninsrc/realtime/chat.js:

'usestrict';

module.exports=io=>{

io.on('connection',(socket)=>{

socket.on('chatMessage',(message)=>{

io.emit('chatMessage',{

username:socket.request.user.name,

message:message

});

});

});

}

NoticethatSocket.IOallowsustosendobjectsinsteadofsimplestringsastheeventpayload.Nowwejustneedtomakeuseofthisintheclientasgivenheresrc/public/scripts/chat.js:

$(document).ready(function(){

'usestrict';

varsocket=io();

...

socket.on('chatMessage',function(data){

$('#messages').append(

$('<p>').text(data.message)

.prepend($('<b>').text(data.username)));

});

Ifyounowopentheapplicationinseparatebrowsersessionsandspecifydifferentusernames,youwillseetheseinthechatoutput.

DirectingSocket.IOmessagesNowthatwehaveaccesstousernames,wecanalsoannouncethearrivalofusersinthelobby.WecandothisbyextendingourSocket.IOconnectioneventhandlerasgivenheresrc/realtime/chat.js:

'usestrict';

module.exports=io=>{

io.on('connection',(socket)=>{

constusername=socket.request.user.name;

if(username){

socket.broadcast.emit('chatMessage',{

username:username,

message:'hasarrived',

type:'action'

});

}

socket.on('chatMessage',(message)=>{

io.emit('chatMessage',{

username:username,

message:message

});

});

});

}

Here,weusesocket.broadcast.emit,ratherthanio.emit,tosendtheeventtoallclientsexceptforthecurrentsocket.Notethatwealsoaddextradatatothemessage.Thistimeweaddatypefield(setto'action'forthearrivalmessage)toallowdifferentvisualpresentationofdifferenttypesofmessage.Wecanachievethisbyupdatingourclient-sidecodetosetadditionalCSSclassesbasedonthemessagetypeasgivenheresrc/public/scripts/chat.js:

socket.on('chatMessage',function(data){

$('#messages').append(

$('<p>').text(data.message).addClass(data.type)

.prepend($('<b>').text(data.username)));

});

Tip

YoucanfindtheCSSfilefortheexampleapplicationinthecompanioncode.

Let'salsoenforcethatusershavetochooseausernamebeforetheycantakepartinthechatasgivenheresrc/realtime/chat.js:

'usestrict';

module.exports=io=>{

io.on('connection',(socket)=>{

...

socket.on('chatMessage',(message)=>{

if(!username){

socket.emit('chatMessage',{

message:'Pleasechooseausername',

type:'warning'

});

}else{

io.emit('chatMessage',{

username:username,

message:message

});

}

});

});

}

Here,weusesocket.emitratherthanio.emittosendamessagetotheclientassociatedwiththecurrentsocket.

TestingSocket.IOapplicationsNowlet'slookathowwecantestourchatmodule.Totalktoitfromourtestswe'llneedaSocket.IOclient.TheSocket.IOprojectprovidesanotherpackageforthis:

>npminstallsocket.io-client--save-dev

Theinfrastructureforourtestsconsistsofsettingupaserverandmultipleclientsasgivenheretest/realtime/chat.js:

'usestrict';

describe('chat',function(){

constexpect=require('chai').expect;

letserver,io,url,createUser,createdClients=[];

beforeEach(done=>{

server=require('http').createServer();

server.listen((err)=>{

if(err){

done(err);

}else{

constaddr=server.address();

url='http://localhost:'+addr.port+'/chat';

io=require('socket.io')(server);

require('../../src/realtime/chat.js')(io);

done();

}

});

});

afterEach(done=>{

createdClients.forEach(client=>client.disconnect());

server.close(done);

});

constcreateClient=require('socket.io-client');

createUser=(name,room)=>{

letuser={

name:name,

client:createClient(url)

};

createdClients.push(user.client);

returnuser;

};

});

Here,wecreateanHTTPserverwithoutspecifyinganaddress,sothattheOSwillassignusanavailableport.Wethenusethisthisservertohostourchatimplementation.

Sincewe'rerunningthechatmoduleinisolation,wedon'thaveourusersmiddlewareavailable,

sowillneedanalternativewaytoprovideusernames.Wecandothiswithastubmiddlewareinourteststhatreadsusernamesdirectlyfromaheader:

'usestrict';

describe('chat',function(){

constexpect=require('chai').expect;

letserver,io,url,createUser,createdClients=[];

beforeEach(done=>{

server=require('http').createServer();

server.listen((err)=>{

if(err){

done(err);

}else{

constaddr=server.address();

url='http://localhost:'+addr.port;

io=require('socket.io')(server);

io.use((socket,next)=>{

socket.request.user={

name:socket.request.headers.username

};

next();

});

require('../../src/realtime/chat.js')(io);

done();

}

});

});

...

constcreateClient=require('socket.io-client');

createUser=(name,room)=>{

letheaders={};

if(name){

headers.username=name;

}

letuser={

name:name,

client:createClient(url,{extraHeaders:headers})

};

createdClients.push(user.client);

user.client.emit('joinRoom',room);

returnuser;

};

});

Nowwearereadytoimplementourtests.Thefirsttwo,formessagesinitiatedfromtheserver,

arequitesimple:

it('warnsunnameduserstochooseausername',done=>{

letunnamedUser=createUser();

unnamedUser.client.emit('chatMessage','Hello!');

unnamedUser.client.on('chatMessage',(data)=>{

expect(data.message).to.contain('chooseausername');

expect(data.username).to.be.undefined;

expect(data.type).to.equal('warning');

done();

});

});

it('broadcastsarrivalofnamedusers',done=>{

letconnectedUser=createUser();

letnewUser=createUser('User1');

connectedUser.client.on('chatMessage',(data)=>{

expect(data.message).to.contain('arrived');

expect(data.username).to.equal(newUser.name);

expect(data.type).to.equal('action');

done();

});

});

Testingmessagessentbetweenclientsrequiresalittlemorecaretocaptureeachclient'sreceiptofthemessage:

it('emitsmessagesfromnamedusersbacktoallusers',done=>{

letnamedUser=createUser('User1');

letotherUser=createUser();

letmessageReceived=function(data){

this.received=data;

if(namedUser.received&&otherUser.received){

[namedUser.received,otherUser.received]

.forEach(received=>{

expect(received.message).to.equal('Hello!');

expect(received.username)

.to.equal(namedUser.name);

});

done();

}

};

otherUser.client.on('chatMessage',

messageReceived.bind(otherUser));

namedUser.client.on('chatMessage',

messageReceived.bind(namedUser));

namedUser.client.emit('chatMessage','Hello!');

});

OrganizingSocket.IOapplicationsNowthatwehaveachatlobbyontheindexpageofourapplication,it'sabitoddthatusershavetoreloadthepage(andlosethechathistory)tofindoutaboutnewgames.WecanuseSocket.IOtoupdatetheseaswell.

Exposingreal-timeupdatestothemodelFirst,we'llneedourgamesserviceitselftoexposeeventsforwhengamesareaddedorremoved.HereweusetheMongoose-providedpostmethodtohookintopersistenceoperationsongamesasgivenheresrc/services/games.js:

'usestrict';

constEventEmitter=require('events');

constemitter=newEventEmitter();

module.exports=(mongoose)=>{

letGame=mongoose.models['Game'];

if(!Game){

letSchema=mongoose.Schema;

letgameSchema=newSchema({

word:String,

setBy:String

});

...

gameSchema.post('save',game=>

emitter.emit('gameSaved',game));

gameSchema.post('remove',game=>

emitter.emit('gameRemoved',game));

Game=mongoose.model('Game',gameSchema);

}

return{

...

get:id=>Game.findById(id),

events:emitter

};

};

module.exports.events=emitter;

Weexposeaneventemittertoallowothermodulestosubscribetoeventsforwhengamesareaddedorremoved.Eventemittersareabuilt-infeatureofNode.js,whichprovideasimplewaytoexposecustomevents.NotethattheMongooseSchemaclassisitselfaneventemitter,sowecouldjustexposethisdirectly.However,thiswouldbeleakingdetailsabouttheimplementationofourgamesservice.

Tip

Again,youcanfindnewtestsforthesechangesinthecompanioncode.

OrganizingSocket.IOapplicationsusingnamespacesReal-timechatandreal-timeupdatestothelistofgamesarequitedistinctfunctionalareasofourapplication.Socket.IOprovidesnamespacestoallowustoorganiseevents.Thisallowsustostilluseasingleconnectionbetweentheclientandtheserver,withouthavingtoworryaboutclashingeventnamesbetweendifferentfunctionalareas.Thisisveryusefulasapplicationsbecomelargerandmorecomplex.

Puttingourchatfunctionalityunderanamespaceisaverysimplechangeontheclientandtheserver(andinourtests).

Thefollowingcodeisfromsrc/public/scripts/chat.js:

$(document).ready(function(){

'usestrict';

varsocket=io('/chat');

...

Thefollowingcodeisfromsrc/realtime/chat.js:

'usestrict';

module.exports=io=>{

constnamespace=io.of('/chat');

namespace.on('connection',(socket)=>{

...

socket.on('chatMessage',(message)=>{

if(!username){

...

}else{

namespace.emit('chatMessage',{

username:username,

message:message

});

}

});

});

};

Thefollowingcodeisfromtest/realtime/chat.js:

constaddr=server.address();

url='http://localhost:'+addr.port+'/chat';

NowwecanaddanewSocket.IOmoduleforexposingchangestogames.ThissimplyneedstoforwardeventsfromourgamesservicetoconnectedSocket.IOclients.

Weaddthefollowingcodeundersrc/realtime/games.js:

'usestrict';

module.exports=(io,service)=>{

io.of('/games').on('connection',(socket)=>{

forwardEvent('gameSaved',socket);

forwardEvent('gameRemoved',socket);

});

functionforwardEvent(name,socket){

service.events.on(name,game=>{

if(game.setBy!==socket.request.user.id){

socket.emit(name,game.id);

}

});

}

};

Wealsoneedtoincludethismoduleintheinitialisationofourserver.

Thefollowingcodeisfromsrc/server.js:

'usestrict';

module.exports=require('./config/mongoose').then(mongoose=>{

...

require('./realtime/chat')(io);

constgamesService=require('./services/games.js')(mongoose);

require('./realtime/games')(io,gamesService);

...

returnserver;

});

Thecorrespondingclientjustneedstoconnecttothe/gamesnamespaceandupdatethelistaccordingly.

Thefollowingcodeisfromsrc/public/scripts/index.js:

varsocket=io('/games');

varavailableGames=$('#availableGames');

socket.on('gameSaved',function(game){

availableGames.append(

'<liid="'+game+'"><ahref="/games/'+game+'">'+

game+'</a></li>');

});

socket.on('gameRemoved',function(game){

$('#'+game).remove();

});

Thefollowingcodeisaddedtosrc/views/index.hjs:

<h3>Gamesavailabletoplay</h3>

<ulid="availableGames">

{{#availableGames}}

<liid="{{id}}"><ahref="/games/{{id}}">{{id}}</a></li>

{{/availableGames}}

</ul>

Tip

Inpractice,itwouldbettertouseaclient-sideMV*librarysuchasKnockoutorBackbonetoupdatethepagebasedonmodelchanges,ratherthanmanipulatingtheDOMlikethis,butthat'soutsidethescopeofthisbook.

Now,ifyouopentheapplicationintwoseparatebrowsersessionsandcreateanewgameinonebrowserwindow,itwillimmediatelyappearintheother.

PartitioningSocket.IOclientsusingroomsThefinalpieceoffunctionalitywe'regoingtoaddinthischapteristheabilityforusersplayingthesamegametotalktooneanother.Wecanre-usethechatfunctionalitywe'vealreadywrittenforthis.However,wewantaseparatechatforthelobbyonthehomepageandforeachgame.

Socket.IOprovidesroomsfordirectingmessagestodifferentgroupsofclients.Rememberthatnamespacesallowustodivideourapplicationintodifferentfunctionalareas.Roomsallowustodivideupclientswithinthesamefunctionalarea.

RoomsinSocket.IOarejuststringidentifiersandweaddclientstoaroomusingthesocket.joinfunction.We'llintroduceanewjoinRoomeventtoallowourclientstoaskourservertoaddthemtoaparticularroom.We'llrespondtothiseventontheserverasfollows:

Thefollowingcodeisfromsrc/realtime/chat.js:

'usestrict';

module.exports=io=>{

constnamespace=io.of('/chat');

namespace.on('connection',(socket)=>{

constusername=socket.request.user.name;

socket.on('joinRoom',(room)=>{

socket.join(room);

if(username){

socket.broadcast.to(room).emit('chatMessage',{

username:username,

message:'hasarrived',

type:'action'

});

}

socket.on('chatMessage',(message)=>{

if(!username){

...

}else{

namespace.to(room).emit('chatMessage',{

username:username,

message:message

});

}

});

socket.on('disconnect',()=>{

if(username){

socket.broadcast.to(room).emit('chatMessage',{

username:username,

message:'hasleft',

type:'action'

});

}

});

});

});

};

Notethatwealsoannouncewhenusersleaveaparticularroom,inthesamewaythatweannouncearrivals.Again,youcanfindtheadditionaltestforthisfunctionalityintheexamplecode.

We'lladdthechatfunctionalityintothegamepageandspecifythecorrectroomusingadataattributeonthechatform.

Thefollowingcodeisfromsrc/views/game.hjs:

<!DOCTYPEhtml>

<html>

<head>

<title>Hangman-Game#{{id}}</title>

<linkrel="stylesheet"href="/stylesheets/style.css"/>

<script

src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js">

</script>

<scriptsrc="/scripts/game.js"></script>

<scriptsrc="/socket.io/socket.io.js"></script>

<scriptsrc="/scripts/chat.js"></script>

<basehref="/games/{{id}}/">

</head>

<body>

<h1>Hangman-Game#{{id}}</h1>

<h2id="word"data-length="{{length}}"></h2>

<p>Pressletterkeystoguess</p>

<h3>Missedletters:</h3>

<pid="missedLetters"></p>

<hr/>

<h3>Discussion</h3>

<formclass="chat"data-room="{{id}}">

<divid="messages"></div>

<inputid="message"/><inputtype="submit"value="Send"/>

</form>

</body>

</html>

Thefollowingcodeisfromsrc/views/index.hjs:

<hr/>

<h3>Lobby</h3>

<formclass="chat"data-room="lobby">

<divid="messages"></div>

<inputid="message"/><inputtype="submit"value="Send"/>

</form>

Thenweneedtoupdatetheclientscripttojointhecorrectroomwhenconnecting.

Thefollowingcodeisfromsrc/public/scripts/chat.js:

$(document).ready(function(){

'usestrict';

varchat=$('form.chat');

varsocket=io('/chat');

socket.emit('joinRoom',chat.data('room'));

chat.submit(function(event){

...

});

...

});

Finally,weneedtomakesurethattypingachatmessagedoesn'tinterferewithplayingthegame.Wecandothisbyonlytreatingkeypressesasguessesforthegamewhentheuserisn'ttypinginthechatmessagebox.

Thefollowingcodeisfromsrc/public/javascript/game.js:

$(document).keydown(function(event){

if(!$('.chat#message').is(':focus')&&

event.which>=65&&event.which<=90){

varletter=String.fromCharCode(event.which);

if(guessedLetters.indexOf(letter)===-1){

guessedLetters.push(letter);

guessLetter(letter);

}

}

});

Tip

Youcanfindnewandupdatedtestsforthisfunctionalityinthecompanioncode.

Puttingthisalltogether,wecannowhavemultipleclientstalkingtooneanotherinseparaterooms:

SummaryInthischapter,wehavecreatedareal-timeclient/servercommunicationchannelusingSocket.IO,usedRedisasabackendtoscaleareal-timeapplicationhorizontally,integratedSocket.IOwithExpressmiddleware,andorganizedourapplicationusingSocket.IOnamespacesandrooms.

Asthenetworkconnectivityofourapplicationisbecomingmorecomplicated,it'smoreimportanttotesttheapplicationonawebserveroutsideofthedevelopmentorCIenvironment.Inthenextchapter,we'lllookathowtodeployourapplicationtotheweb.

Chapter11.DeployingNode.jsApplicationsSofar,wehaveonlyrunourapplicationinourlocaldevelopmentenvironment.Inthischapter,wewilldeployittotheWeb.Therearemanydifferentoptionsforhostinganapplication.Wewillworkthroughonedeploymentoptiontoquicklygetanapplicationupandrunning.WewillalsodiscussbroaderprinciplesandalternativeoptionsfordeployingNode.jsapplications.

Inthischapter,wewillcoverthefollowingtopics:

DeployingourapplicationtotheWebUsingapplicationlogstodiagnoseissuesonremoteserversSettingupdatabaseserversandenvironmentalconfigurationDeployingautomaticallyfromTravisCI

Tip

Ifyouwanttofollowalongwiththischapter,youcanusethecodefromhttps://github.com/NodeJsForDevelopers/chapter10/asastartingpoint.ThiscontainstheexamplecodefromtheendofChapter10,CreatingReal-timeWebApps,whichwewillbuildoninthischapter.

WorkingwithHerokuHerokuisacloud-basedplatformforwebapplications.Itaimstoallowdeveloperstofocusonapplicationsratherthaninfrastructure.Itprovidesalow-frictionworkflowfordeployinganewapplicationquickly,whilealsosupportinglong-termscalability.Italsooffersamarketplaceofadd-onservices,suchasdatabasesandmonitoring.

ThereareseveralsimilarservicestoHeroku,someofwhichwewillcoverlaterinthischapter.Herokuwasoneofthefirstservicesofitskind.Inparticular,itwasoneofthefirsttosupportNode.jsasafirst-classcitizen.Italsooffersmanyfeaturesforfree,includingeverythingneededfortheworkedexampleinthissection.

Note

NotethatHeroku'sfreefeaturesaresufficientfordeployinganapplicationfordevelopment,demonstration,orexperimentalpurposes.Itwouldnotbesufficientforaproductiondeploymentofanapplicationservingendusers.Seehttps://www.heroku.com/pricingfordetailsofHeroku'spricingtiers.

SettingupaHerokuaccountandtoolingTofollowtheexampleinthissection,youwillfirstneedtosignupforHerokuathttps://signup.heroku.com/.

Wewillalsobeusingtheherokutoolbelt,aCLIforconfiguringHeroku.Downloadandinstalltheversionforyourplatformfromhttps://toolbelt.heroku.com/.

Checkthattheherokutoolbeltisinstalledcorrectlyandavailableonyourpath.Openanewcommandpromptandrunthefollowingcommand:

>heroku

Youshouldseethehelptextwithalistofavailablecommands.ConfigurethetoolbelttouseyourHerokuaccountbyrunningthefollowingcommand:

>herokulogin

RunninganapplicationlocallywithHerokuHerokurequiresasmallconfigurationfile(similarto.travis.yml)tellingithowtorunourapplication.ThisisafilenamedProcfile,whichinourcasecontainsasinglelineasfollow:

web:npmstart

ThistellsHerokuthatourapplicationconsistsofasinglewebprocess,whichcanbestartedwithnpmstart.

Note

Note,especiallyifyouareusedtotheWindowsfilesystem,thattheuppercasePinthefilenameisimportant.TheapplicationwillbedeployedtoaUnix-likesystem,wherefilenamesarecase-sensitive.

ToverifyourProcfile,wecanrunourapplicationlocallyusingHeroku:

>herokulocal

ThiswilllaunchourapplicationusingtheProcfile.Notethatitalsosetsadefaultportof5000.Youshouldnowbeabletovisittheapplicationathttp://localhost:5000.

Theherokulocalcommandalsosetsupenvironmentvariablesforourapplication.Thesearereadfromalocal.envfileattherootofourapplication:

MONGODB_URL=mongodb://localhost/hangman

REDIS_URL=redis://127.0.0.1:6379/

YoucantestthisbystartinguplocalinstancesofMongoDBandRedis.Runthefollowingcommandsinseparateprompts(settingthe--dbpathasappropriate):

>redis-server

>mongod--dbpathC:\data\mongodb

>herokulocal

Havingthis.envfilemeansthatwecanusenpmstartdirectly(aswehavebefore)torunwithmockdatastoresandherokulocalwhenwewantamorerealisticenvironment,withouthavingtokeeptrackofourcurrentenvironmentvariables.

DeployinganapplicationtoHerokuNowthatwehavecreatedaProcfile,deployingourapplicationtothewebiseasy.First,weneedtocreateanewHerokuapplication:

>herokucreate

Bydefault,thisprovisionsaminimalapplicationonHeroku,witharandomlyassignedname.Youcanoptionallyspecifyanapplicationnameasathirdparameter.

ThiscommandalsoreturnsthepublicURLforournewly-createdapp,whichwecanvisitnow.Thefollowingresponseisreturned:

There'snotmuchtoseebecausewehaven'tdeployedanythingyet.ThequickestwaytodeployanapplicationtoHerokuisviaGit.TheherokucreatecommandalsocreatedanewGitremoteforustopushto.YoucanseethisbyviewingthelistofGitremotes:

>gitremote-v

WenowhaveaGitremotenamedheroku.MakesurethenewProcfilehasbeencommitted.Now,whenwepushourmasterbranchtothisremote,itisautomaticallybuiltanddeployed:

>gitpushherokumaster

Ifwevisittheapplication'sURLagainnow,weseethefollowing:

Ourapplicationhasdeployedbutisnowreturninganerror.Todiagnosetheproblemwithourapplication,we'llneedtolookatthelogs.

WorkingwithHerokulogs,config,andservicesWecanviewthelogsfromourapplicationbyrunningherokulogs.Ifyoulookthroughthelogstotheerrorstacktrace,you'llseethefollowingerrormessage:

app[web.1]:Error:Cannotfindmodule'mockgoose'

ThemockgoosepackageisunavailablebecauseHerokubuildsourapplicationusingthedependenciesinpackage.jsonandnotthedevDependencies.RecallfromChapter9,PersistingData,thatthiserrorisintentional.WewantedtheapplicationtofailinliveenvironmentsifnoMongoDBURLisconfigured.

Tofixthiserror,weneedtosetupaMongoDBinstanceandconfigureourapplicationtoconnecttoit.We'llneedtodothesameforourRedisDB.BothofthesedatastoresareavailableasservicesfromtheHerokumarketplace.

SettingupMongoDB

WecanaddHerokumarketplaceservicesviathecommandline.MongoLabisathird-partyserviceprovidingMongoDBinstances.Wecanaddaninstancetoourapplicationasfollows:

>herokuaddons:createmongolab:sandbox

Thiscreatesasandbox(freetier)MongoDBinstance,suitablefordemopurposes.NotefromtheoutputofthiscommandthatitalsocreatedaMONGOLAB_URIconfigvariable.Herokuwillprovidethistoourapplicationasanenvironmentvariableatruntime.

OurapplicationisexpectinganenvironmentvariablenamedMONGODB_URL.We'llneedtocreatethisandsetittothesamevalueasMONGOLAB_URI.Youcanviewandsetconfigvariablesforanapplicationasfollows:

>herokuconfig

>herokuconfig:setMONGODB_URL=mongodb://...

YoushouldfillinthevalueofMONGODB_URLtomatchthevalueofMONGOLAB_URIreturnedbythefirstcommand.

SettingupRedis

HerokualsoprovidesaRedisserviceviaitsmarketplace.We'lladdittoourapplicationasfollows:

>herokuaddons:createheroku-redis:hobby-dev--as:REDIS

Againweusethefreetierversionofthisservice(hobby-dev)fordemopurposes.It'seasytore-scaleservicestodifferenttierslater.

TheRedisservicealsoallowsyoutospecifyanaliasforthecreatedserviceinstance.Aliases

arespecifiedusingthe--asparameterwithherokuaddons:create.ThisisusefulforRedisaswemayhaveseveralRedisinstancesassociatedwithasingleapplication.It'sparticularlyusefulforus,since,byaliasingourinstanceasREDIS,HerokuwillcreateaREDIS_URLenvironmentvariable.Thisisexactlywhatourapplicationexpectstosee.

Theherokuaddons:createcommandrestartsourapplicationimmediately.Ournewdatabaseinstanceswilltakeaminuteortwotobecomeavailablethough.Waitaminutebeforerestartingtheapplication:

>herokurestart

WecannowvisittheapplicationURLinourbrowserandseeitrunningontheWeb!

DeployingfromTravisCIDeployingviaGitisaquickwaytogetupandrunningandisusefulfordevelopers.It'snotarobustwayofpushingoutchangesthough.IfwearepracticingContinuousDeliverythenwemaywanttodeployoneverycommit,atleasttoaUATenvironment.ButwestillwantourCIservertoactasagatekeeperandensurethatweonlydeploygoodbuilds.

TravisCIsupportsdeploymenttoawiderangeofhostingproviders(aswellasarbitrarydeploymentviacustomscripts).WecantellTravisCItodeploytoHerokubyaddingadeploysectiontoourtravis.ymlasfollows(replacingapplication-name-12345withthenameofourpreviouslycreatedHerokuapplication):

services:

-mongodb

-redis-server

deploy:

provider:heroku

app:application-name-12345

api_key:

env:

global:

-MONGODB_URL=mongodb://localhost/hangman

-REDIS_URL=redis://127.0.0.1:6379/

TravisCIwillonlydeployourapplicationifthebuildpasses.InorderforTravisCItocommunicatewithHeroku,itrequiresourHerokuAPIkey.Butwemaynotwanttocommitthistosourcecontrol(especiallyifourGitrepositoryispublic).TravisCIallowsyoutoavoidthisbyspecifyingencryptedenvironmentvariablesforthebuild.

SettingencryptedTravisCIenvironmentvariablesEnvironmentvariablescanbeencryptedusingapublickeythatTravisCIassociateswithourrepository.TravisCIthenusesthecorrespondingprivatekeytodecryptthesevariablesatbuildtime.

TheeasiestwaytoencryptenvironmentvariableswiththecorrectkeyistousetheTravisCLI.ThisisavailableasaRubypackage.

InstallingRuby

IfyoudonothaveRubyinstalledonyoursystemalready,seehttps://www.ruby-lang.org/en/documentation/installation/.ThebestwaytoinstallonWindowsistouseRubyInstaller,fromhttp://rubyinstaller.org/.

YoucancheckwhetherRubyisinstalledandconfiguredonyourpathbyrunningthefollowingcommand:

>ruby-ver

Youshouldhaveversion2.0.0orhigher.

Creatinganencryptedenvironmentvariable

OnceyouhaveRubyinstalledandonyourpath,youcaninstalltheTravisCLIasfollows:

>geminstalltravis--no-rdoc--no-ri

Note

GemistheRubypackagemanager,similartonpm.The--no-docand--no-riargumentshereskipinstallationoflow-levelAPIdocs,whichwedon'tneed.

Nowwecanaddourencryptedenvironmentvariable.FirstweneedtoobtaintheHerokuAPIkeyforourapplication:

>herokuauth:token

Nowwecanaddthistoour.travis.ymlfileasfollows:

>travisencrypt[AUTH_TOKEN]--adddeploy.api_key

[AUTH_TOKEN]istheoutputfromthepreviouscommand.

ThisencryptstheAPIkeyandautomaticallyaddstheencryptedversionintoour.travis.ymlfile.Beforecommitting,tryupdatingsomethingintheapplication,forexamplethepagetitlefromsrc/routes/index.js:

...

.then(results=>{

res.render('index',{

title:'Hangmanonline',

userId:req.user.id,

createdGames:results[0],

...

Nowcommitandpushthemasterbranch(toorigin,notdirectlytoheroku)andwaitfortheTravisCIbuildtocomplete.Thebuildoutputshowsourapplicationbeingdeployed:

Ifyouvisittheapplicationagain,youshouldseethenewversionwiththeupdatedtitle.

RecallthatTravisCIisactuallybuildingourapplicationformultipleversionsofNode.js.Bydefault,TravisCIdeploysourapplicationattheendofeachbuildjob.Thisisunnecessaryandslowsdownouroverallbuild.WecantellTravisCItodeployonlyfromaspecificbuildjobbyalteringour.travis.ymlfileasfollows:

deploy:

provider:heroku

app:afternoon-cliffs-85674

on:

node:6

api_key:

secure:...

IfwecommitandchecktheoutputfromTravisCIagain,wecanseethatonlytheNode.jsv6buildjobperformsadeployment.

FurtherresourcesForfurtherconsiderationsondeployingwebapps,seeTheTwelve-FactorApp(http://12factor.net/).Thisisadetailedresourceaboutimportantconsiderationsforrunningenterprise-gradewebapplicationsonservicessuchasHeroku.

Thereare,ofcourse,agreatmanyoptionsforhostingawebapplication.Azure'swebappserviceandAWS'sElasticBeanstalkbothsupportNode.jsasafirst-classcitizen.Modulus(https://modulus.io/)providesNode.jsandMongoDBhosting,withpowerfulscaling,monitoring,andload-balancingfeatures.

Theprecedingareallexamplesofapplicationhostingplatforms(Platform-as-a-Service(PaaS),incloudterminology).Youcan,ofcourse,alsodeployNode.jsapplicationstobareinfrastructure(eithercloudinfrastructureoryourownmachines).Foradetailedguide,seehttps://certsimple.com/blog/deploy-node-on-linux.

Youmayneedtomanagereleasesofyourapplicationthroughmultipleenvironments.YourCIservermightfirstdeployyourapplicationtoanintegrationenvironmentandruntestsonittherebeforedeployingtoUAT.YoumaythenwanttobeabletopushtheexactsamereleasefromUATtoStageandLiveenvironmentsattheclickofabutton.

HerokuPipelinesandAzureWebAppdeploymentslotsallowyoutomanagethereleaseofyourapplicationthroughdifferentenvironments.Wercker(http://wercker.com/)isabuildanddeploymentservicethatcanautomatemorecomplexworkflows.ItalsoprovidesisolatedenvironmentsbasedonDockercontainers.

SummaryInthischapter,wehavedeployedanapplicationtothewebusingHeroku,configuredenvironmentsettingsandprovisioneddatabases,setupTravisCItoautomaticallydeploysuccessfulbuilds,andlearnedaboutfurtheroptionsandconsiderationsfordeployingNode.jsapplications.

Nowthatourapplicationisavailableonline,wecanstartthinkingabouthowtointegrateitwiththewiderWeb.Inthenextchapter,we'lllookatallowinguserstologinusingthirdpartysocialmediaservicesasanidentityprovider.

Chapter12.AuthenticationinNode.jsTheapplicationwehavebuiltsofarallowsuserstochooseausernametoidentifythemselves.However,theyonlyretainthisidentityforthedurationoftheirbrowsersession.It'simportanttoallowuserstoretainaconsistentidentityfromonesessiontothenext.Thisallowsustobuildricheruserexperiences.Somewebsites(suchasFacebook)couldn'toffertheirmainfunctionalityatallwithoutbeingabletoidentifyusers.

Identifyingusersrequiresustoimplementauthentication.Inthischapter,wewillcoverthefollowingtopics:

Implementingthird-partyauthenticationviasocialnetworkingsitesAssociatingthird-partyidentitieswithourownuserdataSimulatinguserauthenticationtosupportintegrationtesting

IntroducingPassportPassportisanauthenticationframeworkforNode.js.ItcanactasExpressmiddleware,makingiteasytointegratewithourapplication.

Likesomeoftheotherlibrarieswe'vediscussedsofar,Passportisverymodular.Itscorepackageprovidesacommonparadigmforauthentication.Passport'smiddlewareperformsauthenticationandaugmentstherequestobjectwithauserproperty.

AdditionalPassportnpmpackagessupporthundredsofdifferentstrategiesforauthentication.EachPassportstrategyprovidesadifferentmechanismforidentifyingusers.We'lllookatafewofthesestrategiesinthischapter.Passportmakesiteasytoaddnewstrategiestosuittheneedsofeachapplication.

ChoosinganauthenticationstrategyAcommonintroductoryexampleisusername/password-basedauthentication.Thisusesaloginformtoverifyusers'credentialsagainsttheapplication'sdatabase.Althoughthisisoneofthesimplestauthenticationmechanismstounderstand,it'snotthemostuseful.Forcinguserstocreateanaccountforoursiteisanextrahurdletothemusingit.Usersalsogettiredofcreatinganaccountandpickingapasswordforeverynewwebsite.

Passportdoessupportthiskindofauthentication,viathepassport-localstrategy.We'llmakeuseofthisstrategyfortestpurposeslateroninthischapter,butnotinourproductioncode.It'sbettertoallowuserstoauthenticateusinganidentityalreadyestablishedelsewhere.Thissavesusersfromhavingtopicknewcredentialsandalsosavesourwebsitefromhavingtomanagethese.Thisisjustgoodseparationofconcerns.

IfyoulogintoStackOverflow,you'llnoticethatitsuggestslogginginusingGoogle+orFacebook.ItalsosupportsOpenIDandotherproviders.Implementingsupportforeachoftheseloginmechanismsfromscratchwouldbealotofwork.FortunatelytherearePassportstrategiesforallofthem.

Understandingthird-partyauthenticationPassportwilldomostoftheheavyliftingforus,butit'sstillworthhavingabasicunderstandingofhowthird-partyauthenticationworks.Whenaclientwantstologintoawebsite,itsendsthemtoathird-partyprovider.Thethird-partyprovidergivestheclientbackatokentheycanusetoauthenticatewiththewebsite.Whentheclientisawebbrowser,thisprocesscanbemadealmostinvisibletotheuser,viaautomaticredirects.

Thewebsitemustthenverifythatthetokenpresentedtoitbytheclientreallycamefromthethird-partyprovider.Thewebsiteandthethird-partyprovidermighthaveestablishedapre-sharedkeyforthispurpose,whichcouldbeusedtocreateacryptographicallyverifiabletoken.Alternatively,thewebsitemightcallthethird-partyproviderdirectlytoverifythetoken.Inpractice,awebsitewilloftenwanttocallathird-partyprovideranywaytogainmoreinformationassociatedwiththeuser'sidentity,forexample,theirusernameorotherprofileinformation.

UsingExpresssessionsManyofPassport'sstrategiesarebasedonHTTPsessions.Atthemoment,ourapplicationisjustusingsimplecookiestostoreuserIDs.TousePassportforthird-partyauthentication,we'llneedtoaddsessionsupportintoourapplication.Expressprovidessessionsupportintheexpress-sessionmodule.First,weaddthistoourapplication:

>npminstallexpress-session--save

Wealsoneedsomewheretostoresessiondata.Expresssupportsavarietyofsessionstoresviaadditionalmodules.RedisiswellsuitedtothistaskandwealreadyhaveaRedisinstanceavailable.Wecanusetheconnect-redismoduletostoresessionsinRedis:

>npminstallconnect-redis--save

Wecannowcreateanewconfigurationmoduletokeepalloursessionlogicinoneplace.Sincethiswillreturnmiddleware,we'llputitinthemiddlewarefolderheresrc/middleware/sessions.js:

'usestrict';

constsession=require('express-session');

letconfig={

secret:process.env.SESSION_SECRET,

saveUninitialized:false,

resave:false

};

if(process.env.REDIS_URL&&process.env.NODE_ENV!=='test'){

constRedisStore=require('connect-redis')(session);

config.store=newRedisStore({url:process.env.REDIS_URL});

}

module.exports=session(config);

WeconfiguretheExpresssessionmoduleasfollows:

UsethevalueofanenvironmentvariableasthesessionsecretOnlysavesessionsthatcontainsomedataDonotresavesessionsunlesstheyhavechangedIfRedisisavailable,useitasthesessionstore

Let'sconsidereachoftheconfigurationpropertiesinturn.

SpecifyingasessionsecretExpressusesasessionsecrettoprotectsessiondatafrombeingtamperingwith.YoushouldspecifythisbysettingtheSESSION_SECRETenvironmentvariablelocally.Thevalueisarbitraryandcanbeanything,aslongasit'snotempty.WealsoneedtospecifythisinourintegrationtestsoitcanrunontheCIserver.Thefollowingcodeisfromgulpfile.js:

gulp.task('integration-test',...,(done)=>{

constTEST_PORT=5000;

process.env.SESSION_SECRET=

process.env.SESSION_SECRET||'testOnly';

require('./src/server.js').then((server)=>{

...

});

});

DecidingwhenthesessiongetssavedAvoidingunnecessarysavesisaminoroptimizationandcanavoidcertainraceconditions.Onlysavinginitializedsessionsallowsyoutorequestuserconsentbeforestoringanycookies.Thismightbenecessaryforcompliancewithregionallaws,mostnotablyintheEU.Seehttps://www.cookiechoices.org/formoreinformation.

UsingalternativesessionstoresBydefault,Expresswilluseanin-memorysessionstore.Thisisfinefordevelopmentpurposesandintestenvironmentswhereweonlyhaveoneapplicationprocess,butisnotsuitableforproductionuse.StoringsessionsoutofprocessinRedisisimportantifwewanttoscaleacrossmultipleinstances.WeconfiguretheRedisstorewithourexistingRedisURL.

Note

Inpractice,youmightwanttousedifferentRedisinstancesforsessiondataandotherapplicationdata.Thesearequitedifferentusecases,sotheymightbenefitfromadifferentconfigurationofRedis.Forexample,sessiondataislikelytobehigherload,butcanaffordtobemorevolatile.Forsmall-scaleapplicationssuchasourexampleapplicationinthisbook,asingleRedisinstancewillsuffice.

UsingsessionmiddlewareWecannowusesessionselsewhereinourapplicationinsteadofdirectlysettingcookies.Thefollowingcodeisfromsrc/app.js:

letsessions=require('./middleware/sessions');

...

app.use(bodyParser.urlencoded({extended:false}));

app.use(sessions);

app.use(express.static(path.join(__dirname,'public')));

...

Thefollowingcodeisfromsrc/middleware/users.js:

'usestrict';

module.exports=(service)=>{

constuuid=require('uuid');

returnfunction(req,res,next){

letuserId=req.session.userId;

if(!userId){

userId=uuid.v4();

req.session.userId=userId;

req.user={

id:userId

};

next();

}else{

...

}

};

};

Thefollowingcodeisfromsrc/server.js:

'usestrict';

module.exports=require('./config/mongoose').then(mongoose=>{

...

io.use(adapt(require('./middleware/sessions')));

constusersService=require('./services/users.js');

...

});

ImplementingsocialloginForourfirstexample,we'lluseTwitterasourthird-partyauthenticationprovider.IfyouwanttofollowalongwiththeexampleyouwillneedaTwitteraccount,whichisveryquicktosetup.

SettingupaTwitterapplicationInorderforTwittertorecognizeourapplication,weneedtocreateanewappinTwitter'sdeveloperportal:

1. Visithttps://apps.twitter.com/andclickonCreateNewApp.2. FillintheName,Description,Website,andCallbackURLfields:

Ifyou'vedeployedyourapplicationtoHeroku,youcanuseitsHerokuURLhereOtherwise,justfillinplaceholdervaluesforbothfields(forexample,http://test.example.com/callback)

3. ClickonCreateyourTwitterapplication.4. ClickontheSettingstabandensurethatEnableCallbackLockingisunchecked(leaving

thisuncheckedallowsyoutouseplaceholdervaluesfortheURLsandisalsousefulforlocaltesting).

5. ClickontheKeysandAccessTokenstabtoviewyourapplication'sConsumerKey(APIKey)andConsumerSecret(APISecret).

SetnewlocalenvironmentvariablesnamedTWITTER_API_KEYandTWITTER_API_SECRET,containingthecorrespondingvaluesfromTwitter.YoumightwanttocreateashellscriptorbatchfiletosettheseintheconsoleorconfigurethemasHerokuenvironmentvariables(seeChapter11,DeployingNode.jsApplications)

ConfiguringPassportWe'llnowmakeuseofPassporttoallowuserstologintooursiteviaTwitter.First,weneedtoinstalltherelevantnpmpackages:

>npminstallpassport--save

>npminstallpassport-twitter--save

NowwecanconfigurePassporttoauthenticatewithTwitter.Weaddthefollowingcodeundersrc/config/passport.js:

'usestrict';

constpassport=require('passport');

constTwitterStrategy=require('passport-twitter').Strategy;

module.exports=(usersService)=>{

if(process.env.TWITTER_API_KEY&&

process.env.TWITTER_API_SECRET){

passport.use(newTwitterStrategy({

consumerKey:process.env.TWITTER_API_KEY,

consumerSecret:process.env.TWITTER_API_SECRET,

callbackURL:'/auth/twitter/callback',

passReqToCallback:true

},(req,token,tokenSecret,profile,done)=>{

usersService.setUsername(req.user.id,

profile.username||profile.displayName)

.then(()=>{done();},done);

}));

}

returnpassport;

};

ThisusestheTwitterStrategyforauthenticationwithTwitter,passinginourAPIkeyandsecretonaconfigurationobject.ThesecondconstructorparameterisafunctionthatPassportwillinvokeafterauthenticatingwithTwitter(referredtoastheverifycallbackinPassport'sdocumentation).Herewesetthecurrentuser'snamebasedontheprofile.usernameorprofile.displayNameprovidedfromTwitterbyPassport.

Note

Theprofileobjectcontainstheuserprofilereturnedbytheauthenticationprovider.Passportstandardizesprofiledatatomakeiteasiertoworkwithmultiplestrategies.There'sastandardsetoffields,suchasdisplayName,whichallPassportstrategieswillpopulateifpossible.We'dprefertousetheTwitterusername(forexample,hgcummings)thanthedisplayname(forexample,HarryCummings).Theprofile.usernamefieldcontainstheTwitterusername.Thisisnotoneofthestandardfields,butmanystrategieswillreturnafieldwiththisname.Soweuseprofile.usernamefirst,butfallbacktothemorestandardprofile.displayName.

NowwejustneedtomakeuseofournewpassportmoduleinExpress.Thefollowingcodeis

fromsrc/app.js:

letpassport=require('./config/passport')(usersService);

...

app.use(users);

app.use(passport.initialize());

app.post('/auth/twitter',passport.authenticate('twitter'));

app.get('/auth/twitter/callback',

passport.authenticate('twitter',

{successRedirect:'/',failureRedirect:'/'}));

app.use('/',routes);

...

Thistellsourapplicationtodothreethings:

UsePassport'sExpressmiddlewareAuthenticateusersviaTwitterwhentheyPOSTto/auth/twitterHandleTwitterauthenticationresultsat/auth/twitter/callbackbeforeredirectinguserstothehomepage

Finally,weneedtoprovidealoginbuttontoreachournewendpointasshownhereinsrc/views/index.js:

<h1>{{title}}</h1>

<h2>Account</h2>

{{#ranking}}

...

{{/ranking}}

<formaction="/auth/twitter"method="POST">

<inputtype="submit"value="LoginusingTwitter"/>

</form>

<h3>Profile</h3>

<formaction="/profile"method="POST">

...

</form>

...

IfyouruntheapplicationandclickLoginusingTwitter,thefollowingwillhappen:

TheapplicationwillredirectyourbrowsertoTwitterTwitterwillpromptyoutologinifyouhavenotalreadyTwitterwillaskwhetheryou'rehappywiththeapplicationseeingyourprofiledetailsandotherpublicdataTwitterwillthenredirectyourbrowsertothe/auth/twitter/callbackendpointYourbrowserwillmakearequesttothisendpointwithyourauthenticationtokenfromTwitterPassportwillvalidatethistokentheninvokeourloginhandlerfunctionWhenourfunctioncompletes,Passportwillreturnaredirectresponsetothehomepage

WehavenowintegratedTwitterauthenticationwithourapplication!However,we'renotreally

usingittoallowuserstologin.We'rejustassociatingaTwitterusernamewithourexistinguserIDscreatedforeachsession.Youcanseethisbyopeninguptwoseparatebrowsersessions.Trylogginginwitheachofthem.Ifyoucreateanewgameinonebrowser,itappearsintheotherbrowserinthelistofgamescreatedbyotherusers.ThisisbecauseyounowhavetwouserIDsassociatedwiththesameTwitterusername.

WeneedtorecognizethesameuserwhenevertheyloginwiththesameTwitteraccount.Thisshouldnotdependonbeinginthesamebrowsersession.Toaddressthis,we'llneedtodothefollowing:

PersistuseraccountstoourdatabaseTellPassporthowtostoreandretrieveusersLetPassportassociateauserwiththecurrentsession

PersistinguserdatawithRedisWealreadyuseRedistoassociateusernameswithuserIDs.NowwewanttobeabletoassociateuserIDswithTwitteraccountsaswell.Thefirsttimeauserlogsinwithanexternalprovider,wewanttocreateanewuserwiththenametakenfromtheexternalprofile.Subsequentrequestsauthenticatedwiththesameproviderwillseethesameuser.

WecanimplementthisfunctionalityusingRedis'sSETNXoperation.Thiswillonlysetakeyifitdoesnotalreadyexistandreturnwhetherthiswasthecase.Ourimplementationisasfollowsfromsrc/services/users.js:

'usestrict';

constredisClient=require('../config/redis.js');

constuuid=require('uuid');

constgetUser=userId=>

redisClient.getAsync(`user:${userId}:name`)

.then(userName=>({

id:userId,

name:userName

}));

constsetUsername=(userId,name)=>

redisClient.setAsync(`user:${userId}:name`,name);

module.exports={

getOrCreate:(provider,providerId,providerUsername)=>{

letproviderKey=`provider:${provider}:${providerId}:user`;

letnewUserId=uuid.v4();

returnredisClient.setnxAsync(providerKey,newUserId)

.then(created=>{

if(created){

returnsetUsername(newUserId,providerUsername)

.then(()=>getUser(newUserId));

}else{

returnredisClient

.getAsync(providerKey).then(getUser);

}

});

},

getUser:getUser,getUsername:userId=>

redisClient.getAsync(`user:${userId}:name`),

setUsername:setUsername,

...

};

Here,wecreateanewuserIDandtellRedistoassociateitwiththeexternalprovider(forexample,Twitter)account.Ifwehaveseentheexternalaccountbefore,wereturntheuserthatwasalreadyassociatedwithit.Otherwise,wepersistanewuserIDandassociateitwiththeusernamefromtheexternalprofile.Testsforthisfunctionalitycanbefoundinthecompanioncode.

ConfiguringPassportwithpersistenceNowthatwehaveawayofpersistingusers,weneedtotellPassporthowtomakeuseofthis.First,weupdateourverifycallbacktomakeuseofournewgetOrCreatefunctionratherthanjustsettingausername.ThenweneedtotellPassporthowtoidentifyandretrieveusersassociatedwithasessionbyserializinguserstoandfromastring.Thefollowingcodeisfromsrc/config/passport.js:

'usestrict';

constpassport=require('passport');

constTwitterStrategy=require('passport-twitter').Strategy;

module.exports=(usersService)=>{

if(process.env.TWITTER_API_KEY&&

process.env.TWITTER_API_SECRET){

passport.use(newTwitterStrategy({

consumerKey:process.env.TWITTER_API_KEY,

consumerSecret:process.env.TWITTER_API_SECRET,

callbackURL:'/auth/twitter/callback',

passReqToCallback:true

},(req,token,tokenSecret,profile,done)=>{

usersService.getOrCreate('twitter',profile.id,

profile.username||profile.displayName)

.then(user=>done(null,user),done);

}));

}

passport.serializeUser((user,done)=>{

done(null,user.id);

});

passport.deserializeUser((id,done)=>{

usersService.getUser(id)

.then(user=>done(null,user))

.catch(done);

});

returnpassport;

};

Passportstoresthestringversionoftheuser(returnedbyourserializeUsercallback)onthesession.ItusesourdeserializeUsercallbacktoturnthisstringintoauserobjectwhichitaddstotherequest.Inourcase,thestringrepresentationoftheuserisjusttheirIDanddeserializationisjustalookupintheusersservice.

Inorderforthistowork,wealsoneedtotellourapplicationtousePassport'sownsessionmiddleware,whichworkstogetherwithExpresssessions.Toavoidrepetition,we'llspecifyallofoursession-relatedmiddlewareinoursessionmiddlewaremodule.Thefollowingisthecodefromsrc/middleware/sessions.js:

...

constexpressSession=session(config);

module.exports=passport=>[

expressSession,passport.initialize(),passport.session()

];

Thismodulenowreturnsthreemiddlewareinstances.WewanttousethiswithbothExpressandSocket.IO.Thefirstoftheseissimple,sincewecanpassmultiplemiddlewareobjectstotheExpressapp.usefunctionasheresrc/app.js:

...

letpassport=require('./config/passport')(usersService);

letsessions=require('./middleware/sessions')(passport);

...

app.use(bodyParser.json());

app.use(bodyParser.urlencoded({extended:false}));

app.use(sessions);

app.use(express.static(path.join(__dirname,'public')));

app.post('/auth/twitter',passport.authenticate('twitter'));

...

ForSocket.IO,weneedtoadapteachmiddlewareinturnasheresrc/server.js:

...

constusersService=require('./services/users.js');

letpassport=require('./config/passport');

require('./middleware/sessions')(passport).forEach(

middleware=>io.use(adapt(middleware)));

require('./realtime/chat')(io);

...

Notethat,inbothcases,ourusersmiddlewareisnolongerneededandcannowbedeleted.However,thismiddlewarepreviouslyensuredthattherewasalwaysauserobjectontherequest.Thiswillnowonlybethecasewhenthereisaloggedinuser,soweneedtoupdatetherestofourapplicationaccordingly.

Thereareafewplacesinourapplicationthatassumetherewillalwaysbeauserontherequest.Sincethisisnolongerguaranteed,therearetwowaystoresolvethis:wecanupdateourcodetocopewithnouserbeingpresentontherequestorwecanhidefunctionalityfromunauthenticatedusers.

Westillwantunauthenticateduserstobeabletoviewpublicchatandtoseeandplaygames,soweupdatethisfunctionalityaccordingly.Thecodefromsrc/realtime/chat.jsisupdatedasfollows:

namespace.on('connection',(socket)=>{

letusername=null;

if(socket.request.user){

username=socket.request.user.name;

}

...

Thefollowingcodeisfromsrc/realtime/games.js:

functionforwardEvent(name,socket){

service.events.on(name,game=>{

if(!socket.request.user||

game.setBy!==socket.request.user.id){

socket.emit(name,game.id);

}

});

}

Thefollowingcodeisfromsrc/routes/games.js:

router.post('/:id/guesses',function(req,res,next){

checkGameExists(

req.params.id,

res,

game=>{

if(req.user&&game.matches(req.body.word)){

userService.recordWin(req.user.id);

}

...

},

next

);

});

HidingfunctionalityfromunauthenticatedusersWecertainlywantunauthenticateduserstobeabletovisitthehomepageofourapplication,butmightnotwanttodisplayalloftheapplication'sfunctionalitytothem.Toachievethis,we'llupdateourindexrouteasfollowsfromsrc/routes/index.js:

router.get('/',function(req,res,next){

letuserId=null;

if(req.user){

userId=req.user.id;

}

Promise.all([gamesService.createdBy(userId),

gamesService.availableTo(userId),

usersService.getUsername(userId),

usersService.getRanking(userId),

usersService.getTopPlayers()])

.then(results=>{

res.render('index',{

title:'Hangmanonline',

loggedIn:req.isAuthenticated(),

createdGames:results[0],

...

});

})

.catch(next);

});

NotethatthisaddsaloggedInpropertytotheviewdatainsteadoftheuserID.ThevalueofthispropertycomesfromtheisAuthenticatedfunction,whichisaddedtotherequestbyPassport.Weusethistohidefeaturesthatwillnolongerworkforunauthenticatedusersandhidetheloginbuttonfromauthenticatedusers.Thefollowingcodeisfromsrc/views/index.hjs:

...

<body>

...

{{^loggedIn}}

<formaction="/auth/twitter"method="POST">

<inputtype="submit"value="LoginusingTwitter"/>

</form>

{{/loggedIn}}

{{#loggedIn}}

<h3>Profile</h3>

<formaction="/profile"method="POST">

...

</form>

{{/loggedIn}}

<h2>Games</h2>

{{#loggedIn}}

<formaction="/games"method="POST"id="createGame">

...

</form>

<h3>Gamescreatedbyyou</h3>

...

{{/loggedIn}}

<h3>Gamesavailabletoplay</h3>

...

<h2>Topplayers</h2>

...

<h3>Lobby</h3>

<formclass="chat"data-room="lobby">

<divid="messages"></dl>

{{#loggedIn}}

<inputid="message"/><inputtype="submit"value="Send"/>

{{/loggedIn}}

</form>

</body>

</html>

IntegrationtestingwithPassportWestillhaveoneproblem,whichisthatourintegrationtestswon'tworkanymore.Onlylogged-inuserscancreategamesnow.ItwouldbeagoodideatowriteanewintegrationtesttocheckthatTwitterauthenticationworks.Wedon'twanttointroduceaTwitteraccountdependencytoourcurrenttestthough.

Instead,we'llmakeuseofthepassport-localstrategytoallowourtesttologin.We'llinstallthisasadevdependencysoitcan'taccidentallyruninproduction:

>npminstallpassport-local--save-dev

WeconfigurePassporttoacceptanyusernameandpassword.Ifusingpassport-localforreal,thisiswhereyouwouldcheckagainstcredentialsinyourdatastore.Thefollowingcodeisfromsrc/config/passport.js:

if(process.env.NODE_ENV==='test'){

constLocalStrategy=require('passport-local');

constuuid=require('uuid');

passport.use(newLocalStrategy((username,password,done)=>{

constuserId=uuid.v4();

usersService.setUsername(userId,username)

.then(()=>{

done(null,{id:userId,name:username});

});

}

));

}

Thenweaddanewlocalauthenticationendpointtoourapplicationasheresrc/app.js:

if(process.env.NODE_ENV==='test'){

app.post('/auth/test',

passport.authenticate('local',{successRedirect:'/'}));

}

Andfinallyupdateourtesttologinasafirststepascodefromintegration-test/game.jsshownfollows:

functionwithGame(word,callback){

page.open(rootUrl+'/auth/test',

'POST',

'username=TestUser&password=dummy',

function(){

...

}

);

}

AllowinguserstologoutUserswillalsoexpectustoprovideawaytologoutofourapplication.Passportmakesthiseasybyaddingalogoutfunctiontotherequest.Wejustneedtomakeuseofthisinoneofourroutesheresrc/routes/index.js:

router.post('/logout',function(req,res){

req.logout();

res.redirect('/');

});

Wecanaddalogoutbuttontoourviewtomakeuseofthisnewrouteasinsrc/views/index.hjs:

{{#loggedIn}}

<formaction="/logout"method="POST">

<inputtype="submit"value="Logout"/>

</form>

<h3>Profile</h3>

AddingotherloginprovidersNowthatwehaveallthegeneralinfrastructureforauthentication,addingadditionalprovidersiseasy.Let'saddFacebookauthenticationasanexample.First,weneedtoinstalltherelevantPassportstrategy:

>npminstallpassport-facebook--save

ThenwecanupdateourPassportconfigfilefromsrc/config/passport.jsasfollows:

...

constFacebookStrategy=require('passport-facebook').Strategy;

module.exports=(usersService)=>{

constproviderCallback=providerName=>

function(req,token,tokenSecret,profile,done){

usersService.getOrCreate(providerName,profile.id,

profile.username||profile.displayName)

.then(user=>done(null,user),done);

};

if(process.env.TWITTER_API_KEY&&

process.env.TWITTER_API_SECRET){

passport.use(newTwitterStrategy({

consumerKey:process.env.TWITTER_API_KEY,

consumerSecret:process.env.TWITTER_API_SECRET,

callbackURL:'/auth/twitter/callback',

passReqToCallback:true

},providerCallback('twitter')));

}

if(process.env.FACEBOOK_APP_ID&&

process.env.FACEBOOK_APP_SECRET){

passport.use(newFacebookStrategy({

clientID:process.env.FACEBOOK_APP_ID,

clientSecret:process.env.FACEBOOK_APP_SECRET,

callbackURL:'/auth/facebook/callback',

passReqToCallback:true

},providerCallback('facebook')));

}

...

};

Herewe'vegeneralizedourverifycallbackfunctiontotakedifferentprovidernames,thenusedthiswithbothTwitterandFacebookauthenticationstrategies.Wecanre-usethistoaddfurtherstrategiesinthesameway.Wejustneedtosettherelevantenvironmentvariablesforthemtowork.

Note

ToobtainaFacebookAppIDandSecret,createanewFacebookapplicationathttps://developers.facebook.com/apps/(whichrequiresyoutohaveaFacebookaccount).Thisis

verysimilartotheprocessforTwitter.JustcreateanewapplicationoftypeWebsite,withaURLthatmatchesyourdevelopmentenvironment(forexample,http://localhost:3000).Oncecreated,theAppIDandAppSecretwillbevisibleontheDashboardpagefortheapplication.

WealsoneedtoaddFacebookauthenticationroutestoourapplicationconfigfile.ThesearejustthesameasthecorrespondingTwitterroutes.AswiththePassportconfigfile,wecancommonizebyparameterizingtheprovidername.Thecodefromsrc/app.jsisasfollows:

app.use(sessions);

constaddAuthEndpoints=provider=>{

app.post(`/auth/${provider}`,passport.authenticate(provider));

app.get(`/auth/${provider}/callback`,

passport.authenticate(provider,{successRedirect:'/',

failureRedirect:'/',session:true}));

};

addAuthEndpoints('twitter');

addAuthEndpoints('facebook');

Finally,weneedtoaddabuttontoallowuserstologinwithFacebook.Thefollowingcodeisfromsrc/views/index.hjs:

{{^loggedIn}}

<formaction="/auth/twitter"method="POST">

<inputtype="submit"value="LoginusingTwitter"/>

</form>

<formaction="/auth/facebook"method="POST">

<inputtype="submit"value="LoginusingFacebook"/>

</form>

{{/loggedIn}}

Addingadditionalprovidersiseasy.ToaddGoogle+authentication,wewouldjustneedtofollowthesesteps:

1. Installthepassport-googlenpmmodule2. Createanewapplicationasdescribedat

https://developers.google.com/identity/protocols/OpenIDConnect3. Updatethethreefileslistedabove,passingtheGoogleprovidertoournewcommon

functions

SummaryInthischapter,wehaveaddedauthenticationtoourExpressapplicationusingPassport,introducedExpresssessionsusingRedisforsessionstorage,leveragedmultiplePassportstrategiestosupportdifferentexternalproviders,andpersisteduserdatainRedis.

Thiscompletesourexamplewebapplication.InthenextchapterwewilllookathowtocreatedifferentkindsofNode.jsproject:alibraryandacommand-linetool.

Chapter13.CreatingJavaScriptPackagesSofarwehavebuiltupawebapplication,makinguseofvariousnpmpackagesalongtheway.ThesepackagesincludelibrariessuchasExpressandcommand-linetoolssuchasGulp.Nowwe'lllookathowtogoaboutcreatingpackagesofourown.

Inthischapterwewill:

ExplorethedifferentmodulesystemsavailableforJavaScriptCreateourownJavaScriptlibraryWriteJavaScriptthatcanrunonboththeclientandserver-sideCreateacommand-linetoolinJavaScriptReleaseanewnpmpackageUseNode.jsmodulesinthebrowserenvironment

Note

Thecodeexamplesinthischapterareindependentofeverythingwe'vedonesofar.

WritinguniversalmodulesWehavealreadywrittenmanyofourownmodulesaspartofourapplication.Wecanalsowritelibrarymodulesforuseinotherapplications.

Whenwritingcodeforusebyothers,it'sworthconsideringinwhatcontextsitwillbeuseful.Somelibrariesareonlyusefulinspecificenvironments.Forexample,Expressisserver-specificandjQueryisbrowser-specific.Butmanymodulesprovidefunctionalitythatwouldbeusefulinanyenvironment,forexample,utilitymodulessuchastheuuidmodulewe'veusedelsewhereinthisbook.

Let'slookatwritingamoduletoworkinmultipleenvironments.We'llneedtosupportmorethanjustNode.js-stylemodules.We'llalsoneedtosupportclient-sidemodulesystemssuchasRequireJS.RecallfromChapter4,IntroducingNode.jsModules,thatNode.jsandRequireJSimplementtwodifferentmodulestandards(CommonJSandAsynchronousModuleDefinition(AMD),respectively).Ourpackagemayalsobeusedclient-sideinawebsitewithnomodulesysteminplace.

Asanexample,let'screateamoduleprovidingasimpleflatMapmethod.ThiswillworklikeSelectManyin.NET'sLINQ.Itwilltakeanarrayandafunctionthatreturnsanewarrayforeachelement.Itwillreturnasinglearrayofthecombinedresults.

AsaNode.js/CommonJSmodule,wecouldimplementthisasfollows:

module.exports=functionflatMap(source,callback){

returnArray.prototype.concat.apply([],source.map(callback));

}

ComparingNode.jsandRequireJSRecallfromChapter4,IntroducingNode.jsModules,thateachmodulesystemprovidesthefollowing:

AwayofdeclaringamodulewithanameanditsownscopeAwayofdefiningfunctionalityprovidedbythemoduleAwayofimportingamoduleintoanotherscript

Node.jsimplementstheCommonJSmodulestandard.Modulenamescorrespondtofilepathsandeachfilehasitsownscope.Modulesdefinethefunctionalitytheyprovideusingtheexportsalias.Modulesareimportedusingtherequirefunction.

RequireJSisdesignedforthebrowserenvironment.Inthebrowserthereisnonewscopeperfile(allscriptfilesexecuteinthesamescopeandcanseethesamevariables).Also,modulesmustbeloadedbynetworkrequestsratherthanfromthelocalfilesystem.

RequireJSimplementstheAMDstandard.AMDspecifiestwofunctions,whichRequireJSaddstothetop-levelwindowobjectinthebrowserenvironment:

Thedefinefunctionallowsnewmodulestobecreatedbyprovidinganameandafactoryfunctionforthemodule.Thescopeofthemodulewillbethescopeofitsfactoryfunction.Thefunctionalityofthemoduleisdefinedbythereturnvalueofthefactoryfunction.Therequirefunctionallowsmodulestobeimported.AlthoughthishasthesamenameasthemoduleimportfunctioninNode.js,itworksverydifferently.Multiplemodulenamescanbespecifiedforimport(asanarray).Therequirefunctionisasynchronousandtakesacallbacktobeexecutedwhenallthedependenciesareloaded.ThisallowsRequireJStoloadmodulesefficientlyinthebrowserenvironment.

SupportingthebrowserenvironmentForourmoduletoworkinthebrowserenvironment,weneedtosupporttheAMDstandardsoRequireJScanwork.Wealsoneedtoaccommodatesitesnotusinganymoduleloader.Wecanachievethisbyextendingourmoduledefinitionasfollows,inscripts/flatMap.js:

(function(root,factory){

'usestrict';

if(typeofdefine==='function'&&define.amd){

define([],factory);

}elseif(typeofmodule==='object'&&module.exports){

module.exports=factory();

}else{

root.flatMap=factory();

}

}(this,function(){

'usestrict';

returnfunctionflatMap(source,clbk){

returnArray.prototype.concat.apply([],source.map(clbk));

}

}));

Note

Notetheuseofananonymousfunctionthatisinvokedstraightaway,calledanImmediately-InvokedFunctionExpression(IIFE).ThisisacommonwayofcreatinganisolatedscopeinJavaScriptenvironmentswithoutbuilt-inmodules.

First,wecheckfortheexistenceofanAMD-styledefinefunction(theexistenceofadefine.amdpropertyisalsospecifiedbytheAMDstandard).Notethattheasynchronousnatureofthedefinefunctionmeansthatweneedtouseafactoryfunctiontocreateourmodule.Weprovidealistofdependencies(emptyinthiscase)andourfactoryfunctiontothedefinefunctiontocreateourmodule.

IfnoAMDmodulesystemispresent,wecheckfortheCommonJS-stylemodule.exportsusedbyNode.js.Finally,ifneithermodulesystemispresent,weprovideourmoduleasapropertyontherootparameter.Ourargumentforthisparameteristhethiskeywordevaluatedintheglobalscope.Inabrowser,thiswillbethewindowobject.

UsingAMDmoduleswithRequireJSLet'screateasimplewebpagetocheckthatourmoduleworkscorrectlywithRequireJS.We'llalsoshowhowtouseRequireJSwithanexternallibrary,jQuery.

FirstwedefineanHTMLfileforthepage:

<!DOCTYPEhtml>

<html>

<head>

<scriptdata-main="scripts/main"

src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.1.22/require.min.js"

></script>

<style>input,pre{display:block;margin:0.5emauto;width:320px;

}</style>

</head>

<body>

<inputtype="text"/>

<inputtype="text"/>

<inputtype="text"/>

<inputtype="text"/>

<preid="wordcounts"></pre>

</body>

</html>

NotethattheonlyscripttagonthepageisforRequireJSitself.Thisscripttagalsohasadataattributeindicatingtheentrypointofourapplication.Thepathscripts/maintellsRequireJStoloadscripts/main.js,whichcontainsthefollowing:

requirejs.config({

paths:{

jquery:

'https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.1/jquery.min'

}

});

require(['flatMap','jquery'],function(flatMap,$){

$('input').change(function(){

varallText=$.map($('input'),function(input){

return$(input).val();

}).filter(function(text){

return!!text;

});

varallWords=flatMap(allText,function(text){

returntext.split('');

});

varcounts={};

allWords.forEach(function(word){

counts[word]=(counts[word]||0)+1;

});

$('#wordcounts').text(JSON.stringify(counts));

})

});

ThisscriptfirstconfiguresRequireJS.Theonlyconfigpropertyspecifiedhereisthepathproperty.ThepathforjQueryunderthekey'jquery'tellsRequireJShowtoresolvethe'jquery'dependency.Wedon'tneedtospecifyapathforflatMap.jsbecausewehavesaveditunderthesamedirectoryasmain.js.

NextweusetherequirefunctiontoloadflatMapandjQueryandpassthemintoourmainapplicationfunction.InlargerapplicationsusingRequireJS,thisisusuallyaveryshortbootstrapfunction.Themain.jsfileisalsooftentheonlyplacethatyou'llseearequirecall.Mostoftheapplicationcodeisinmodulesdeclaredwithdefine.

AsthisisjustatestofourlibrarywithRequireJS,we'llputtherestofourapplicationcodeinsideourmainapplicationfunction.WeuseourflatMapmoduleandjQuerytocalculateanddisplaywordcountsacrossallthetextinputs.Youcanseethisworkingbyopeningindex.htmlinyourbrowser:

IsomorphicJavaScriptTheflatMap.jsexampleaboveisanimplementationoftheUniversalModuleDefinitionpattern.Seehttps://github.com/umdjs/umdforannotatedtemplatesforthispattern.Thesetemplatesalsoshowhowtodeclaredependenciesbetweenmodulesthatfollowthispattern.

Moregenerally,writingcodethatachievesthesameresultbothontheserverandinthebrowserisreferredtoasIsomorphicJavaScript.Seehttp://isomorphic.net/formoreexplanationandexamplesofthisprinciple.

WritingnpmpackagesIfyoucreatesomecodethatwouldbeusefultoothers,youcandistributeitasannpmpackage.Todemonstratethis,we'llimplementsomeslightlymorecomplexfunctionality.

Note

Youcanfindtheexamplecodeforthissectionathttps://github.com/NodeJsForDevelopers/autotoc.Notethat,unlikepreviouschapters,thereisnotonepercommitperheading.Thelistingsintherestofthissectionmatchthefinalversionofthecode.

We'regoingtoimplementatoolforgeneratingatableofcontents(ToC)bycrawlingawebsite.Tohelpwiththis,we'llmakeuseofafewothernpmpackages:

requestprovidesanAPIformakingHTTPrequests,whichishigher-levelandmuchsimplertousethanthebuildintheNode.jshttpmodulecheerioprovidesjQuery-likeHTMLtraversaloutsideofthebrowserenvironmentdenodeify,mentionedinChapter8,MasteringAsynchronicity,allowsustousetherequestlibrarywithpromisesinsteadofcallbacks

Tip

It'scommonfornpmpackagestodependonotherpackagesinthisway.Butitisworthminimizingyourpackage'sdependenciesifyouwantittobeappealingtootherdevelopers.Packageswithmanytransitivedependenciescanaddalotofbloattoapplications,andmakeitharderfordeveloperstobeconfidentthattheyunderstandeverythingtheyarepullingintotheirapplication.

Thecodeforourmodulefollows,asgiveninautotoc.js:

'usestrict';

constcheerio=require('cheerio');

constrequest=require('denodeify')(require('request'));

consturl=require('url');

classPage{

constructor(name,url){

this.name=name;

this.url=url;

this.children=[];

}

spider(){

returnrequest(this.url)

.then(response=>{

let$=cheerio.load(response.body);

letpromiseChildren=[];

$('a').each((i,elem)=>{

letname=$(elem).contents().get(0).nodeValue;

letchildUrl=$(elem).attr('href');

if(name&&childUrl&&childUrl!=='/'){

letabsoluteUrl=url.resolve(this.url,childUrl);

if(absoluteUrl.indexOf(this.url)===0&&

absoluteUrl!==this.url){

letchildPage=newPage(name.trim(),absoluteUrl);

if(childUrl.indexOf('#')===0){

promiseChildren.push(Promise.resolve(childPage));

}else{

promiseChildren.push(childPage.spider());

}

}

}

});

returnPromise.all(promiseChildren).then(children=>{

this.children=children;

returnthis;

});

});

}

}

module.exports=baseUrl=>newPage('Home',baseUrl).spider();

It'snotimportanttounderstandeverysinglelineaswe'remoreinterestedinhowitwillbepackaged.Theimportantpointsare:

WeloadthestartingpagethenfollowlinksthroughtootherpagesandprocesstheserecursivelytobuilduptheentireToCWeonlyfollowlinkstomorespecificURLsthanthecurrentpage(thatis,subpaths),sowedon'tgetintoinfiniteloopsAteachlevel,weloadallchildpagesinparallelandusePromise.alltocombinetheresults

We'llalsoaddasimplemoduletoprintaToCtotheconsole,asgiveninconsolePrinter.js:

'usestrict';

constprintEntry=function(entry,indent){

console.log(`${indent}-${entry.name}(${entry.url})`);

entry.children.forEach(childEntry=>{

printEntry(childEntry,indent+'');

})

}

module.exports=toc=>printEntry(toc,'');

DefiningannpmpackageTodefineannpmpackage,wemustaddafiletoactastheentrypointtoourpackage.Thiswilljustexposetheinnermodulesappropriately,asgiveninindex.js:

'usestrict';

module.exports=require('./autotoc.js');

module.exports.consolePrinter=require('./consolePrinter.js');

Wealsoneedtoaddannpmpackage.jsonfiletodefineourpackage'smetadata.Tocreatethisfile,youcanrunnpminitinthecommandlineandfollowtheprompts.Inourcase,theresultingfilelookslikethefollowing:

{

"name":"autotoc",

"version":"0.0.1",

"description":"Automatictableofcontentsgeneratorforwebsites",

"main":"index.js",

"author":"hgcummings<[email protected]>(http://hgc.io/)",

"repository":"https://github.com/NodeJsForDevelopers/autotoc",

"license":"MIT",

"dependencies":{

"cheerio":"^0.20.0",

"denodeify":"^1.2.1",

"request":"^2.69.0"

}

}

We'veusedpackage.jsonfilesbeforetospecifydependenciesfornpminstall.Theotherfieldsbecomemuchmoreimportantwhenpublishingapackagetonpm.Notethatweusethemainpropertytospecifyourpackage'sentrypoint.Actually,index.jsisthedefaultvalue,butspecifyingitexplicitlymakesthisclearer.

PublishingapackagetonpmOncewehavedefinedourpackage'smetadata,publishingittonpmisverystraightforward:

Ifyoudonotalreadyhaveannpmaccount,createonebyrunningnpmadduserandspecifyingausernameandpasswordLoginusingnpmloginIntherootfolderofthepackage,runnpmpublish

That'sallweneedtodo!Ourpackagewillnowappearintheglobalnpmrepository.Wecanmakeuseofitby(inanewfolder)runningnpminstallautotocandwritingthefollowingsimpledemoscriptasgivenindemo.js:

'usestrict';

constautotoc=require('autotoc');

autotoc('http://hgc.io')

.then(autotoc.consolePrinter,err=>console.log(err));

Runningnodedemo.jsatthecommandlineproducesthefollowingoutput:

RunningautomatedclientsonthewebIt'sfinetoruntoolslikethisagainstyourownwebsite.Therearemanyusecasesforthiskindoftechnique.Forexample,ascriptthatspidersthroughanentiresiteandcheckseverypagecanbeausefulintegration/smoketest.

Usecasesthatinvolvecrawlingsitesthatyoudon'townrequiremorecare.Anypublic-facingsitethatyoucouldvisitinabrowser,youcouldalsoaccesswithanautomatedclientlikethis.Butissuingalargenumberofautomatedrequestsagainstthesamehostisundesirable.ItcouldbeconsideredpooretiquetteatbestoraDenialofService(DoS)attackatworst.

ClientsshouldsetanappropriateUser-AgentHTTPheader.Someserversmightrejectrequestsfromclientsthatdon'tspecifyaUser-Agentordon'tappeartobeabrowser.Byconvention,crawlersshouldsendaUser-AgentincludingthewordbotinthenameandideallyaURLtofindoutmoreaboutthebot.Therequestlibrarymakesiteasytospecifyheadersbypassinginanoptionsobject.Forexample:

letoptions={

url:'http://hgc.io',

headers:{

'User-Agent':'Examplebot/1.0(+http://example.com/why-im-crawling-your-

website)'

}

};

request(options).then(...);

Crawlersshouldalsocheckforarobots.txtfileforeachwebsiteandrespectanyrulesitcontains.Seehttp://www.robotstxt.org/robotstxt.htmlformoreinformation.

Finally,legitimatecrawlersofthird-partywebsitesshouldalsorate-limittheirrequeststoavoidoverwhelmingtheserver.

ReleasingastandalonetooltonpmSomeofthenpmpackageswe'veusedsofarinthisbookhavebeencommand-linetoolsratherthanlibraries,forexampleGulp.Creatingacommand-linetoolpackageisverystraightforward.First,weneedtodefinethescriptthatwewantpeopletobeabletoinvokefromthecommandline,asgivenincli.js:

#!/usr/bin/envnode

'usestrict';

constautotoc=require('./autotoc.js');

constconsolePrinter=require('./consolePrinter.js');

autotoc(process.argv[2])

.then(consolePrinter,err=>console.log(err));

Thislooksmuchlikeourdemoscriptfrombefore,withacoupleofdifferences:

Thelineatthebeginningofthescript(calledashebangline,startingwith#!)indicatestotheOSthatthisscriptshouldbeexecutedusingNode.jsTheURLtocrawlistakenfromacommand-lineargument

Nowwejustneedtospecifythisscriptinourpackage.json:

{

"name":"autotoc",

"version":"0.1.1",

"description":"Automatictableofcontentsgeneratorforwebsites",

"main":"index.js",

"bin":{

"autotoc":"./cli.js"

},

"author":"hgcummings<[email protected]>(http://hgc.io/)","repository":

"https://github.com/NodeJsForDevelopers/autotoc",

"license":"MIT",

"dependencies":{

"cheerio":"^0.20.0",

"denodeify":"^1.2.1",

"request":"^2.69.0"

}

}

Topublishourupdatedpackage,wefirstneedtoupdateourversionnumber.Youcanupdatethisinthepackagedirectlyorusethenpmversioncommand,forexample

>npmversionminor

Thisautomaticallyupdatestheversionnumbertothenextmajor/minor/patchversion(asspecified)andmakesanewgitcommitwiththischange.

Sincewearealreadyloggedintonpm,wecannowpublishthenewversionofourpackagebyrunningnpmpublishagain.

WecannowmakeuseofourCLItoolasfollows(inanewcommandpromptwindow):

>npminstall-gautotoc

>autotochttp://hgc.io

UsingNode.jsmodulesinthebrowserAtthebeginningofthischapter,wediscussedcreatinguniversalmodulesthatcanrununderNode.jsorinthebrowser.Thereisanotherwaythatwecanallowourcodetoruninbothenvironments.

Browserify(http://browserify.org/)allowsyoutomakeuseofNode.jsmodulesinthebrowser.Itbundlesupyourcodetogetherwithitsdependencies.Italsoprovidesbrowser-compatibleshimstoemulateNode.jsbuilt-inmodules.

YoucaninstallBrowserifyvianpm:

>npminstall-gbrowserify

Browserifyistypicallyusedtopackageapplications.Forexample,ifwewantedtopackageourdemousageofautotocfromtheprevioussection,wecouldrun:

>browserifydemo.js-obundle.js

BrowserifywillcreateasingleJavaScriptfilecontainingthecodefromdemo.js,alongwithitsdependenciesandtransitivedependencies.IfweincludethisinanHTMLpage,wecannowseeitworkinginthebrowserconsole:

YoucanalsouseBrowserifytogeneratebrowser-compatiblefilesforindividualmodules,followingtheUniversalModuleDefinitionpatterndiscussedearlierinthischapter.Forexample,tocreateaUMDversionofourautotoc.jsmodulefromtheprevioussection,wecouldrun:

>browserifyautotoc.js-sautotoc-obrowser/scripts/autotoc.js

WecouldnowmakeuseofthisviaRequireJS.Let'screateasimpleapplicationthatusesautotoctogetherwithjQuerytogenerateanHTMLToC.Firstwe'llneedanHTMLfiletocontainourapplicationandincludeRequireJS,asgiveninbrowser/index.html:

<!DOCTYPEhtml>

<head>

<scriptdata-main="scripts/main"

src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.1.22/require.min.js"

></script>

</head>

<body>

</body>

Nowwecanimplementourapplicationitself,asgiveninbrowser/scripts/main.js:

requirejs.config({

paths:{

jquery:'https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.1/jquery.min'

}

});

require(['autotoc','jquery'],function(autotoc,$){

'usestrict';

autotoc('http://hgc.io').then(toc=>{

letprintEntry=function(entry,parent){

letlist=$(document.createElement('ul'));

list.append(

`<li><ahref="${entry.url}">${entry.name}</a></li>`);

entry.children.forEach(childEntry=>{

printEntry(childEntry,list);

})

parent.append(list);

}

printEntry(toc,$('body'));

},err=>console.log(err));

});

Thisresultsinthefollowingoutput:

ControllingBrowserify'soutputNotethat,bydefault,Browserifygeneratesabundleofyourcodeandallofitsdependencies.Includingtransitivedependencies,thiscanresultinaverylargefile.Theautotocmoduleisonly42lineslong,butthegeneratedbundleisover80,000lines!OurapplicationaboveincludesbothjQuery(viaRequireJS)andaversionofCheerio(viaBrowserify).Thisisparticularlywasteful,sincemuchofCheerioisare-implementationofjQuery.

YoucaninstructBrowserifytoexcludespecificmodulesandtoexcludeallexternalmodules.Thisisparticularlyusefulforthird-partymodulesthatfollowtheUMDpattern.Thesedonotneedtobebrowserifiedandcanbeexcludedfromthegeneratedbundle.Youcanthenloadthemseparatelyinthebrowser,viaanadditionalscripttagorusingRequireJS.

FormoreinformationonBrowserify'susageoptions,seetheofficialdocumentationathttps://github.com/substack/node-browserify#usage.

Browserifyprovidesalotofflexibilityforbundlingmodulesindifferentways.Itisparticularlyusefulwhenworkingonasinglecodebasewithbothserver-sideandclient-sidefunctionality.ItallowsyoutowriteallofyourcodeusingNode.js-stylemodulesandtoeasilysharemodulesbetweentheserverandtheclient.

SummaryInthischapter,wehavewrittenamulti-environmentmodulefollowingtheuniversalmoduledefinitionpattern,createdannpmpackageforalibraryandacommand-linetool,andpackagedNode.jscodeforthebrowserusingBrowserify.

ThisdemonstratestheflexibilityofNode.jsandtherangeofusecasesforJavaScriptandnpmbeyondjustserver-sidecode.Inthefinalchapter,we'lllookatthebroadercontextaroundNode.js.We'llseesomeofthenewerlanguagesandupcominglanguagefeaturesfortheplatformandhowNode.jsinteractswithotherplatformslike.NET.

Chapter14.Node.jsandBeyondSofar,thisbookhasshownyouhowtoworkwithJavaScriptandNode.jsinavarietyofusecases.Inthischapter,we'lllookathowtheJavaScriptecosystemiscontinuingtoevolve.We'llalsoseehowthe.NETandJavaScriptecosystemsinfluenceeachotherandhowtointegratethemwithinasingleproject.

WhilethechapterssofarhaveaimedtostartyouonyourpathintoNode.jsandJavaScript,thischapteraimstomapouttheremainingterritory.Eachoftheprecedingchaptershasprovidedin-depthstep-by-stepcoverageofasingletopic.Thischapterwillcoveramuchbroaderrangeoftopics,withlinkstoresourcesforfurtherreading.

Inthischapter,wewill:

UnderstandhowNode.jsandJavaScriptarecontinuingtoevolveIntroducesomeofthenewandupcomingJavaScriptlanguagefeaturesLookatsomealternativeprogramminglanguagesforNode.jsandthewebConsiderprinciplesfromNode.jsthatcanapplyto.NETprogrammingSeehowtointegrateNode.jswith.NET

UnderstandingNode.jsversioningAsmentionedinChapter1,WhyNode.js?,thereleaseofNode.jsv4in2015showstheplatformcomingtomaturity.Ifyou'veusedNode.jsbeforetheendof2015,youwouldhaveseenversionnumberssuchasv0.8.0orv0.12.0.Sowhytheleaptov4.0.0?

AbriefhistoryofNode.jsNode.jsisanopen-sourceprojectwithacorporatesponsor,Joyent.ThismeansthatasinglecompanyhasalotofinfluenceoverthedirectionofNode.js,butanyonecancreatetheirownforkofthesourcecode.Thisisexactlywhathappenedattheendof2014.AgroupofmajorcontributorstoNode.jssplittheprojecttocreateanewfork,namedio.js.Afewkeypropertiesofio.jswere:

AmoreopengovernancemodelAmoreregularreleasecycle,keepingmoreup-to-datewiththeunderlyingV8engine,totakeadvantageofperformanceimprovementsandnewerJavaScriptlanguagefeaturesAmovetosemanticversioning(seehttp://semver.org/),resultinginmajorversionnumbersincreasingmorequickly

Overthecourseof2015,theNode.jsprojectreshapeditselftotakeontheabovepropertiesandalignwithio.js.InSeptember2015,thereleaseofNode.jsv4broughtthetwoprojectsbacktogetherunderanewgovernancemodel.Node.jsv4supersedes(andmerges)bothNode.jsv0.12andio.jsv3.3.Youcanreadmoreaboutthenewgovernancemodelathttps://nodejs.org/en/about/governance/.

IntroducingtheNode.jsLTSscheduleThetimetableforNode.jsreleasesnowfollowsaregularschedule.Anewstablereleaseoccursevery6months.Eachstablebranchreceivesfixesaswellasnewfeaturesthatreachmaturity.Thelifetimeofstablereleasesalternatesasfollows(asshowninthefollowingchart):

Odd-numberedbrancheslivefor9monthsEven-numberedbranchesenterlong-termsupport(LTS)after6months,receivingbugfixesbutnonewfeaturesLong-termsupportlastsfor30months,withthefinal12monthsbeingmaintenancemode(criticalbugfixesonly)

YoucanfindmoredetailsoftheLTSmodelathttps://github.com/nodejs/LTS.

TheLTSmodelallowsyoutohaveconfidenceinNode.jsasaplatformforyourapplication.ThecodeinthisbooktargetsNode.jsv6,thecurrentstablereleaseatthetimeofpublication.ThisversionwillbeinLTSthroughtoApril2019,somethreeyearslater.

UnderstandingECMAScriptversioningECMAScriptistheformalstandardfortheJavaScriptlanguage.Thefirstthreeiterationsofthelanguageoccurredbetween1997and1999.A10-yeargapfollowedbeforeECMAScript5inDecember2009.ES5introducedfewnewfeaturesandfocusedoncleaningupthelanguage.Itintroducedstrictmodesandaddressedvariousinconsistencies,flaws,orgotchasinearlierversions.

2015sawamajorchangetothelanguageandtotheversioningapproach.ECMAScript2015(formerlyECMAScript6)introducedmanysignificantnewlanguagefeatures.Theseincludeclasses,let/constkeywordsandblock-scoping,arrowfunctions,andnativepromises.Intherestofthischapter,we'lllookatsomeoftheothersignificantnewfeaturesinES2015.

ThenamechangefromES6toES2015indicatesanewyearlyversioningmodel.From2015onwards,therewillbeanewversionoftheECMAScriptstandardeveryyear.Plannedfeaturesthataren'tquitereadyforreleasewillwaituntilthefollowingyear.Forthisreason,ECMAScript2016isasmallreleasewithonlyacoupleofnewfeatures.

NotethatECMAScriptisthestandardandittakestimefornewfeaturestobeimplemented.Indeed,someES2015featuresarestillmissingfromtheJavaScriptenginesinpopularbrowsers.NotethoughthatthemajorbrowservendorsarepartoftheECMAScriptstandardsprocess.Sobrowsers,andChrome'sV8engine(usedbyNode.js)inparticular,shouldgenerallynotlagtoofarbehindthelateststandard.

ExploringECMAScript2015WehavealreadyusedmanyofthenewfeaturesofES2015throughoutthisbook,suchasarrowfunctions,templatestrings,andpromises.WehavealsoalreadyseenES2015'ssyntaxforclassesinChapter3,AJavaScriptPrimer.

ES2015isamajorupdatetothelanguage,includingmanynewfeaturesandsyntaximprovements.Thissectionwillcoversomeoftheotherusefulimprovementsthatwehaven'tseensofarinthebook.ForcompletecoverageofeverythingnewinES2015,seetheexcellentExploringES6,availableathttp://exploringjs.com/es6/.

UnderstandingES2015modulesAsmentionedinpreviouschapters,ES2015introducesanewmodulespecification.RecallfromChapter4,IntroducingNode.jsModules,thateachmodulesystemprovidesthefollowing:

AwayofdeclaringamodulewithanameanditsownscopeAwayofdefiningfunctionalityprovidedbythemoduleAwayofimportingamoduleintoanotherscript

Modulesarescopedtotheircontainingfile,asinCommonJS.Modulesprovidefunctionalityviaanewexportkeyword.Prefixinganexpressionwithexportisequivalenttomakingitapropertyofthemodule.exportsvariableinCommonJS.Aspecialdefaultexportisequivalenttoassigningthevalueofmodule.exportsitself.Modulesareimportedusinganimportkeywordratherthanaspecialrequirefunction.Thereisoneadditionalrestriction:importsmustcomeatthetopofthescript,beforeanyconditionalblocksorotherlogic.

Thesemightseemlikesmallsyntaxchanges,buttheyhaveanimportantimplication.Becausedefiningandimportingmodulesdoesn'tinvolveassignmentandmethodcalls,thestructureofdependenciesbetweenmodulesisstatic.ThisallowstheJavaScriptenginetooptimizeloadingofmodules(particularlyimportantinthebrowser).Italsomeansthatcyclicdependenciesbetweenmodulescanberesolved.

YoucanfindoutmoreaboutthenewES2015modulesyntaxathttp://jsmodules.io/.

UsingsyntaximprovementsfromES2015Inthissectionwe'lllookatsomeofthenewsyntaxfeaturesinES2015thatwehaven'tusedinthebooksofar.TheseareallavailableinthelatestJavaScriptengines,includingNode.jsv6.

Thefor...ofloop

Let'ssaywehaveanarraydefinedasfollows:

letmyArray=[1,2,3];

Let'salsosaythatanotherlibraryhasaddedahelperfunctiontoallarrays.PerhapssomethinglikeourflatMapfunctionfromChapter13,CreatingJavaScriptPackages.

Array.prototype.flatMap=function(callback){

returnArray.prototype.concat.apply([],this.map(callback));

};

Ifyouwantedtoiteratethroughallthemembersofanarray,youmightbetemptedtouseJavaScript'sfor...inconstructasfollows:

for(letiinmyArray){

console.log(myArray[i]);

}

Thisdoesn'tworkverywellthough,asitincludespropertiesonthearray'sprototypeandprintsouttheflatMapfunctionaswellastheelementsinthearray.Thisisacommonproblemwithfor...inloops,whenusedwithobjectsaswellaswitharrays.Thestandardwaytoavoiditisbyskippingprototypepropertiesasfollows:

for(letiinmyArray){

if(myArray.hasOwnProperty(i)){

console.log(myArray[i]);

}

}

Thisprintsoutjusttheelementsofthearray,aswewant.Asimilarloopcouldalsobeusedtoprintthepropertiesofanobject,withoutaccidentallyattemptingtoprintoutfunctionsfromtheprototype(whichmayhavebeenaddedbyathird-partylibrary).

Notethatfor...inalsodoesn'ttechnicallyguaranteetheorderinwhichititeratesthroughthekeysofanobject.Thismeansit'snotreallythebestthingtousewitharrays,whereweexpectaspecificorder.That'swhythestandardwaytoiteratethrougharraysisusingaplainoldforloop,asfollows:

for(leti=0;i<myArray.length;++i){

console.log(myArray[i]);

}

ES2015addressestheseissueswithanewfor...ofloop,whichlookslikethis:

for(letvalueofmyArray){

console.log(value);

}

Thesyntaxisverysimilartofor...inloops.However,youdonotneedtofilteroutprototypemembersastheseareexcluded.Itcanbeusedwithanyiterableobjects(suchasarrays)andwillfollowthenaturalorderingoftheiterable.Inshort,for...ofloopsarelikefor...inloopsbutwithoutanynastysurprises.

Thespreadoperatorandrestparameters

Thespreadoperatorallowsyoutotreatarraysasiftheywereasequenceofvalues.Forexample,tocallafunction:

letmyArray=[1,2,3];

letmyFunc=(foo,bar,baz)=>(foo+bar)*baz;

console.log(myFunc(...values));//Prints9

Youcanalsousethespreadoperatorwithinarrayliterals,forexample:

letsubClauses=['2a','2b','2c'];

letclauses=['1','2',...subClauses,'3'];

//Equivalentto['1','2','2a','2b','2c','3']

Therestparametersyntaxservestheoppositepurpose,turningasequenceofvaluesintoanarray.ThisissimilartotheparamskeywordinC#orvarargsinJava.Forexample:

functionfoldLeft(combine,initial,...values){

letresult=initial;

for(letvalueofvalues){

result=combine(result,value);

}

returnresult;

}

console.log(foldLeft((x,y)=>x+y,0,1,2,3,4));//Prints10

Destructuringassignment

Destructuringallowsyoutousestructuringsyntaxtoassignmultiplevariablestogether.Forexample,youcanassignvariablesusingthearrayliteralsyntaxtodestructurearrays:

letfoo,bar;

[foo,bar]=[1,2];//Equivalenttofoo=1,bar=2

Youcanalsocombinedestructuringwiththespreadoperator:

[foo,bar,...rest]=[1,2,3,4,5];

//Equivalenttofoo=1,bar=2,rest=[3,4,5]

Finally,youcanusedestructuringwiththeobjectliteralsyntax:

{foo,bar}={foo:1,bar:2};//Equivalenttofoo=1,bar=2

Destructuringisparticularlyusefulfordealingwithcomplexreturnvalues.Imagineifanyoftheexpressionsontheright-handsideoftheequalssignintheaboveexampleswereactuallyfunctioncalls.

Destructuringisalsousefulforperformingmultipleassignmentsinasinglestatement.Forexample:

[foo,bar]=[bar,foo];//Swapfooandbarinplace

[previous,current]=[current,previous+current];

//CalculationstepforaFibonaccisequence

IntroducinggeneratorsES2016introducesgeneratorfunctionsandtheyieldkeyword.YoumayalreadybefamiliarwiththeyieldkeywordinC#.MethodsthatreturnIEnumerable/IEnumeratorcanincludetheyieldkeywordtoreturnoneelementatatime,suspendingexecutionofthemethoduntilthenextvalueisrequested.YoucandothesamewithgeneratorfunctionsinJavaScript.ThefollowingexampleisaJavaScriptimplementationofoneoftheexamplesfromtheMSDNdocumentationofC#'syield.Itprintsthefirsteightpowersof2(notetheasteriskafterthefunctionkeyword,whichdenotesthisasageneratorfunction):

'usestrict';

function*powers(number,exponent){

letresult=1;

for(leti=0;i<exponent;++i){

result=result*number;

yieldresult;

}

}

for(letiofpowers(2,8)){

console.log(i);

}

Notethatfor...ofloopsworkwithgenerators.Theaboveloopisequivalenttothefollowingcode:

letgenerator=powers(2,8);

letcurrent=generator.next();

while(!current.done){

console.log(current.value);

current=generator.next();

}

YoucanseethatgeneratorsareverysimilartotheIEnumeratorinterfaceinC#.Notethattheyareslightlymorepowerfulthanthisthough.Wecanalsopassavalueintoagenerator'snextmethodtoallowittobeusedwhenexecutioncontinuesinthegeneratorfunction.Thefollowingdummyexampleillustratesthis:

'usestrict';

function*generator(){

letreceived=yield1;

console.log(received);

return3;

}

letinstance=generator();

letfirst=instance.next();

console.log(first);

letlast=instance.next(2);

console.log(last);

Runningthepreviousexampleproducesthefollowingoutput:

>{value:1,done:false}

>2

>{value:3,done:true}

Thistwo-waycommunicationmakesgeneratorsmuchmorethanjustIEnumeratorforJavaScript.Theyareapowerfulcontrolflowmechanism,especiallywhencombinedwithpromises.Seehttps://www.promisejs.org/generators/foraderivationofC#-likeasync/awaitfunctionalityusinggeneratorsandpromises(withyieldtakingtheplaceofC#'sawaitkeyword).It'salsoworthnotingthatasyncfunctionsareplannedforafutureversionofECMAScript(probablyES2017)andwillworkinasimilarway.Inthemeantime,youcanachieveasimilarprogrammingmodelusingthePromise.coroutinemethodprovidedbythebluebirdlibrary,whichisbasedongenerators.Seehttp://bluebirdjs.com/docs/api/promise.coroutine.htmlfordetails.

IntroducingECMAScript2016Asmentionedearlierinthischapter,ECMAScript2016isasmallreleasewithonlyacoupleofnewfeatures.Theseareanincludesmethodforarraysandtheexponentationoperator**.

YoucanwritemyArray.includes(value)insteadofmyArray.indexOf(value)!==-1.Notethattheseexpressionsarenotquiteequivalent.YoucanuseincludestocheckforthevalueNaNwithinanarray,whichyoucan'tdowithindexOf.

TheexponentialoperatorallowsyoutorewriteMath.pow(coefficient,exponent)ascoefficient**exponent.

Youcanalsocombineitwithanassignment,asinmyVariable**=2.

GoingbeyondJavaScriptIfyouwanttotargetbrowsersorNode.js,JavaScriptistheonlylanguagenativelysupportedbytheseenvironments.ThisisdifferenttoVM-basedenvironmentslikethe.NETruntimeandtheJVM,whichsupportmultiplelanguages.

The.NETruntimesupportsC#,F#,VB.NET,andothers.TheJVMsupportsJava,Scala,Clojure,andothers.Theselanguagesworkbycompilingdowntoanassemblylanguagefortheenvironment'sVM.ThisistheCommonIntermediateLanguagein.NETorJavabytecodeinthecaseoftheJVM.

Thereisareasonwhyprogrammersdon'tallwriteCILorJavabytecodethough.Thesearelow-levelmachinelanguagesandmuchlesshuman-friendlythanC#,Java,andsoon.Ingeneral,higher-levellanguagescansupportbetterproductivity,aswellassafety(forexample,throughtypesystemsandmemorymanagement).

Thereisalsoareasonwhy.NETprogrammersdon'talwaysuseC#andJVMprogrammersdon'talwaysuseJava.Arangeoflanguagescanservedifferentusecasesbetter.Itcanalsojustbeamatterofpersonaltasteforthesemanticsofaparticularlanguage.

JavaScripthasbeencalledtheAssemblyLanguagefortheWeb(http://www.hanselman.com/blog/JavaScriptIsAssemblyLanguageForTheWebSematicMarkupIsDeadCleanVsMachinecodedHTML.aspxWhileJavaScriptisnotalow-levelormachinelanguage,itisacommonlanguageforitsplatform.LikeCILandJavabytecode,itcanserveasacompiletargetforotherlanguages.And,like.NETandtheJVM,thereisanappetiteamongstdevelopersforavarietyoflanguagesonthesameplatform.

Exploringcompile-to-JavaScriptlanguagesThereareseverallanguagesthatsupportwebandNode.jsdevelopmentbycompilingdowntoJavaScript.We'lllookatafewofthemoreprominentoftheselanguagesinthissection.

TypeScript

TheTypeScriptlanguageisdevelopedandsupportedbyMicrosoft.Itskeyaimistoincludefeaturesthataidlarge-scaleapplicationdevelopment.TypeScriptcanbecompileddowntoES2016,ES5,orevenES3.SoitworksinanymodernJavaScriptenvironment.

TypeScriptisbasedcloselyontheJavaScriptsyntax.ItisasupersetofJavaScript,soyoucanwriteordinaryJavaScriptandgraduallyuseTypeScriptfeaturesmoreasyoulearnit.TypeScriptalsotriestomatchthesyntaxofupcomingJavaScriptfeatureswherepossible.ThisallowsdeveloperstostartusingnewJavaScriptfeaturesearlier.

ThemostimportantTypeScriptfeaturesaidlarge-scaleapplicationdevelopment.TypeScripthashadclassesandmodulesforsometime,tohelpwithstructuringcode.Asthenamesuggests,TypeScriptalsoaddstypeannotationsandtypeinference.Italsoaddsnewwaysofdefiningandspecifyingtypes,includingenums,generictypes,andinterfaces.Thismakesforasaferlanguageasthecompilercancatchmoreerrors.ItalsoletsIDEsofferfeatureslikecodecompletion(namely,Intellisense)andbettersourcecodenavigation.

Finally,TypeScriptmakesitpossibletospecifytypedefinitionsforlibrarieswritteninplainJavaScript.Typedefinitionsformanythird-partylibrariescanbefoundathttps://github.com/DefinitelyTyped/DefinitelyTyped.Theseprovidetypecheckingandcodecompletionwhenworkingwithlibrarycodetoo.

Here'sanexampleofourflatMapfunctionfromthepreviouschapterwrittenwithtypeannotations:

functionflatMap<T,R>(

source:T[],

callback:(T)=>R[]):R[]{

returnArray.prototype.concat.apply([],

source.map(callback));

}

letresult=flatMap([1,2,3],(i:number)=>[i,i+0.5]);

console.log(result);//Prints[1,1.5,2,2.5,3,3.5]

ThesyntaxforgenericsmaybefamiliarfromC#.Typeannotationsfollowtheexpressionorparameter,separatedbyacolon.Wecouldspecifythegenerictypewhenwecallthefunctiontoo,butinthiscaseitcanbeinferred.Notethatourmethodhastwogenerictypes,asourcallbackcouldmaptoanarrayofadifferentelementtype.TheTypeScriptcompilerwillinferthetypeofresultasnumber[].Notethatthisinferenceactuallytakesafewsteps:

WespecifythatthecallbackparameterihasatypenumberTherefore,theexpressionsiandi+0.5alsobothhaveatypenumber

Therefore,theresulttypeofourcallbackisnumber[]Therefore,theargumentforthetypeparameterRmustbenumber

Ifwedidnotspecifythetypeofi,thenthecompilerwouldonlyinferthetypeofresultasany[],thatisanarray,butofanunspecifiedelementtype.

YoucanlearnmoreaboutTypeScriptathttp://www.typescriptlang.org/.

Tip

Ifyou'remorefamiliarwithJavathan.NET,andespeciallyifyou'refamiliarwiththeEclipseIDEinparticular,youmayalsobeinterestedinN4JS(http://numberfour.github.io/n4js/).ThislanguagehassimilargoalstoTypeScript,butisinspiredbyJavaandhasanIDEbasedonEclipse.

CoffeeScript

CoffeeScriptwasoneoftheearliestsuccessfulcompile-to-JavaScriptlanguages.CoffeeScriptstreamlinesthesyntaxofJavaScriptandaddsfeaturesforwritingmoreterseandexpressivecode.

CoffeeScriptisagoodexampleofwhentastemightinfluencelanguagechoice.DevelopersmayfindCoffeeScriptmorereadableand/oreasiertowrite.RubyorPythonprogrammersmaybeparticularlycomfortablewithCoffeeScript.They'llfinditssyntaxandmanyofitslanguagefeaturesfamiliar.

ManyfeaturesfromCoffeeScripthavesubsequentlyappearedinES2015,forexamplearrowfunctions,destructuring,andthesplat/spreadoperator.UnlikeTypeScript,CoffeeScriptdoesnotattempttomatchthesyntaxofJavaScript,neitherforcurrentnorupcomingfeatures.ItdoeshoweverofferseamlessinteroperabilitywithJavaScriptcode.

ComprehensionsareoneofCoffeeScript'smostexpressivefeaturesanddonotappearinES2015.YoumaybefamiliarwithcomprehensionsfromPython.TheyarealsoalittlelikeLINQinC#,inthattheyallowyoutoexpressoperationsonlistswithoutusingloops.Thefollowingexampleprintsthesquaresofevennumbers,firstinJavaScriptandthenasaone-linerinCoffeeScript.Assquares.js:

vari,n;

for(n=i=1;i<=10;n=++i){

if(n%2===0){

console.log(n*n);

}

}

Assquares.coffee:

console.logn*nfornin[1..10]whenn%2is0

Andbeyond...

TypeScriptandCoffeeScriptarespecificallydesignedtotargetJavaScript.TherearemanyotherprojectsinexistencethatallowmoregenerallanguagestocompileJavaScript.Notethatnotallsuchprojectsarematureorwell-maintained.LanguageswhoseownprojectteamsupportsandmaintainscompilationtoJavaScripttendtobeasaferchoice.BothDart(https://www.dartlang.org/)andClojure(http://clojure.org/)providefirst-classsupportforcompilingtoJavaScript.

IntroducingatrueassemblylanguageforthewebAsdiscussedabove,whileJavaScriptcanbeacommoncompiletargetforthewebandNode.js,itisnotatrueassemblylanguage.Itisahigh-levelhuman-readablelanguage,ratherthananoptimizedmachinelanguage.Thereareprojectstointroducejustsuchalanguageintothewebenvironmentthough.Thismeansdefininganassemblylanguageimplementedbyallbrowsers,includingChrome'sV8engineandthereforeNode.js.

Understandingasm.js

Thefirstattemptatsuchalanguageisasm.js(http://asmjs.org/),developedbyMozilla.ThisisastrictsubsetofJavaScript,whichmeansitcanrunonanybrowser.Butbrowsersthatsupportasm.jscanprecompileitandheavilyoptimizeitsexecution.Demandingapplicationssuchas3Dgamescanberecompiledtotargetasm.jsandrunseamlesslyin-browser.Thefirstenvironmentwithfullsupportforasm.jsisMozilla'sownFirefoxbrowser.ItwillalsobesupportedinMicrosoft'snewEdgebrowser.TheV8engineusedbyChrome(andNode.js)doesnotyetpre-compileasm.js,butV8doesmakesomeoptimizationstoallowasm.jstorunmuchfasterthanifinterpretedasplainJavaScript.

UnderstandingWebAssembly

WebAssembly(https://webassembly.github.io/)isanewstandardforatrueassemblylanguagefortheweb.Unlikeasm.jsitisnotasubsetofJavaScriptandwon'trunintoday'sbrowsers.ItdefinesanewassemblylanguagemorelikeCILorJavabytecode.ItisdevelopedbytheW3Cstandardsbody,withinputfromthemajorbrowservendors.ThereareearlyimplementationsofWebAssemblyinpreviewreleasesofMozillaFirefox,GoogleChrome,andMicrosoftEdge.

Asanapplicationdeveloper,youdonotneedtobeabletowriteWebAssemblyanymorethanyouneedtowriteCILorJavabytecode.Thesearealllow-levellanguagestoactascompilationtargets.Infuture,WebAssemblymayreplaceJavaScriptasthecommoncompiletargetfortheweb(andNode.js).Otherlanguages,includingJavaScriptitself,mayallcompiletoWebAssembly.

ThiswouldmeanthatJavaScriptwouldnolongerbetheonlynativelanguageforthewebandNode.js.ButJavaScriptwillalmostcertainlyremainthedefaultdevelopmentlanguagefortheseenvironments,justasC#andJavaarefortheirrespectiveenvironments.KnowledgeoftheexecutionmodelofNode.jswillstillberelevantinanylanguageandJavaScriptwillstillbethemostnaturalfitforthisexecutionmodel.KnowledgeofJavaScriptwillalsobeimportantforworkingwiththemanywell-establishedlibrariesbasedonit.

TherewouldbeotherbenefitstoJavaScriptfromWebAssembly.InteroperationbetweenJavaScriptandotherlanguageswillbecomeeasier.Therewillbemoreoptionsforimplementingperformance-criticalcode.NewversionsofJavaScriptwillbeabletorolloutmorequickly(asasingleJavaScripttoWebAssemblycompilercantargetallbrowserengines).

JavaScriptandASP.NETOntheserverside,wedon'tneedtowaitforWebAssemblytomatureinordertoworkwithNode.jsand.NETtogether.Thereisalreadysomeconvergencebetweenprogrammingonthesetwoplatformsandsupportforinteroperabilitybetweenthem.

Exploring.NETCoreThenextversionofNET,called.NETCore,makessomemajorchangestotheplatform.Someofthesechangesmightseemfamiliarifyou'vespentsometimeworkingwithNode.js.Thisisnotjustacoincidence.MicrosoftareincorporatinggoodideasthathaveworkedinNode.jsandelsewhereintotheirecosystem.

Definingprojectstructurein.NETCore

.NETCoreseparatestheprogrammingplatformfromtheIDE.MicrosoftstillrecommendsusingVisualStudio,buthavemadeitmucheasiertouseothereditors.Forexample,theOmniSharpproject(http://www.omnisharp.net/)supportsdevelopmentinothereditors,providingfeaturessuchasIntellisenseoutsideofVisualStudio.

Oneaspectofthischangeissimplifyingtheuseof.csprojfiles.Inpreviousversionsof.NET,theselargeXMLfileswerethecanonicaldescriptionofeachC#project.Theyincludedimportantthingslikecompilationoptions,targetplatforms,buildsteps,anddependencies.TheyweremainlygeneratedbyVisualStudio,difficulttoeditbyhand,andoftenparticularlyawkwardtomergeinsourcecontrol.TosatisfyVisualStudio,theyalsoneededtolisteverysinglesourcefileintheproject.

Manyofthesedrawbacksareaddressedin.NETCore.Newtoolsmakeitmucheasiertoedit.csprojfilesfromthecommandline.Aproject'ssourcesarejustthefilesunderitsparentfolder(notlistedin.csprojoranyothermetadatafile).DependenciesaredeclaredseparatelyinamorelightweightJSON-basedfile.

ManyoftheseimprovementsareinspiredbyprogrammingplatformslikeNode.js.Infact,earlyreleasecandidatesfor.NETCoreremovedtheneedfor.csprojfilesentirelyandintroducedproject.jsonfiles(justlikeinNode.js)fordefiningprojects.Although.NETCoreultimatelyuses.csprojfiles(forcontinuedcompatibilitywithMSBuild),itaimstokeepthoseaspectsofmorelightweightapproachesthataremostimportanttodevelopers.

Managingdependenciesin.NETCore

TheNuGetpackagemanagerhasbeenpartofthe.NETecosystemforseveralyears.NuGetbecomesevenmoreimportantin.NETCore.TheframeworkandruntimethemselvesaredistributedasNuGetpackages.DependenciesarespecifiedasNuGetpackagenames(andversions)ratherthanDLLpaths.NuGetpackagescanalsobeausefulunitofdeploymentforyourownprojects.

JustlikewithNode.js,youcancheckoutthesourcecodeofoneofyourdependenciestoalocalfolderandreferenceitthere.Thisallowsyoutotinkerwithopensourcelibrariesanddebugthemaspartofyourprogram.

BuildingwebapplicationsinASP.NETCore

ASP.NETCoreconsolidatesASP.NETMVCandWebAPIintoasingleframework.ItalsobringsOWINtotheforeasthestandardabstractionforimplementingwebapplications.

OWINsimplydefinesastandardforpassingrequestandresponseobjectsbetweenahostandanapplication.AlthoughOWINhasbeenaroundforawhileandhasitsownhistory,thisisasimilarabstractiontothehttp.createServermethodinNode.js.YoucanreadmoreaboutOWINathttps://docs.asp.net/en/latest/fundamentals/owin.html.

Relatedtothis,ASP.NETalsousesmiddlewareasthestandardbuildingblockforwebapplications.Again,althoughmiddlewarein.NEThasitsownhistory,theabstractionisverysimilartomiddlewareinExpress.Applicationssetupapipelineofmiddleware,witheachhavingaccesstotherequest,response,andthenexthandlerinthechain.Built-inmiddlewareisavailableforcross-cuttingconcernssuchasauthentication,sessions,androuting.Youcanreadmoreaboutmiddlewareathttps://docs.asp.net/en/latest/fundamentals/middleware.html

IntegrationwithJavaScriptVisualStudiohasprovidedgoodsupportforclient-sideJavaScriptdevelopmentforseveralyears.MicrosofthaveimprovedandupdatedthisinthelatestversionsofASP.NETandVisualStudio:forexample,byincludingbetterintegrationwithtaskrunnerssuchasGulpandGrunt.Youcanreadmoreaboutclient-sideJavaScriptsupportathttps://docs.asp.net/en/latest/client-side/index.html.

Server-sideJavaScriptintegrationwith.NET

TheEdge.jsproject(https://github.com/tjanczuk/edge)allowsNode.jsand.NETtorunwithinthesameprocess.Italsodefinesaverysimplewayformarshallingmethodcallsbetweenthetwo.Thisismuchfasterthanmarshallingcallsout-of-process(forexample,viaanHTTPcalltoaprocessonthelocalmachine).

Edge.jsallowsyoutotakethebestof.NETandNode.js.PerhapsyouwanttouseNode.jstoputawebinterfaceontopofyourexisting.NETbusinesslogic.Orperhapsyou'reusingNode.jsforrapiddevelopmentofmostofyourapplication,buthaveaparticularlyCPU-intensiveoperationthatwouldbeeasiertooptimizein.NET.

MakingcallsfromNode.jsto.NET(orviceversa)isverysimple.Forexample,ifwehavethefollowing.NETclass:

usingSystem;

usingSystem.Threading.Tasks;

namespaceDeepThought

{

publicclassUltimateQuestion

{

publicTask<Object>GetAnswer(objectinput){

varresult=new

{

description=

"AnswertoTheUltimateQuestionof"+input,

value=42

};

returnTask.FromResult<object>(result);

}

}

}

WecanuseitfromJavaScriptasfollows(afterrunningnpminstalledge):

'usestrict';

constedge=require('edge');

letgetAnswer=edge.func({

assemblyFile:'bin\\Debug\\DeepThought.dll',

typeName:'DeepThought.UltimateQuestion',

methodName:'GetAnswer'

});

getAnswer('Life,theUniverse,andEverything',(error,result)=>{

console.log(result);

});

CompilingourC#codeandrunningourJavaScriptfileresultsinthefollowingoutput:

>nodeindex.js

>{description:'AnswertoTheUltimateQuestionofLife,theUniverse,and

Everything',value:42}

YoucanfindagoodintroductiontoEdge.jsathttp://www.hanselman.com/blog/ItsJustASoftwareIssueEdgejsBringsNodeAndNETTogetherOnThreePlatforms.aspx

Finally,recallthattheOWINstandardandASP.NETmiddlewarearequitesimilartothecorrespondingconceptsinJavaScript.Edge.jsmakesiteasytoincludea.NETOWINapplicationasmiddlewareinaNode.jsExpressapplication.Seetheconnect-owinprojectathttps://github.com/bbaia/connect-owinfordetails.

SummaryInthischapter,wehaveseenhowNode.jsandJavaScript'snewreleasecyclesbringstabilitytotheplatform.WehaveintroducedsomeofthenewandupcomingfeaturesofJavaScript.WehaveexploredcurrentandfuturealternativelanguagesfortheJavaScriptenvironment.Wehaveseensomeofthecommonalitiesbetween.NETandNode.jsandhowtousethesetechnologiestogether.

Ihopethisbookhasallowedyoutogetup-and-runningwithNode.jsandgivenyouanappetitetolearnmore.TheresourcesinthischapterwillhelpyoutakethenextsteponyourjourneywithJavaScriptandNode.js.

IndexA

adapterpattern/UsingRedisasabackendafterEachhook

about/Resettingstatebetweentestsaggregationpipeline

about/UsingtheMongoDBshellAjax

used,forcommunication/CommunicatingviaAjaxalternativesessionstores

using/UsingalternativesessionstoresAMD

about/JavaScriptmodulesystemsAMDmodules

using,withRequireJS/UsingAMDmoduleswithRequireJSapp.jsfile/ExploringourExpressapplicationapplication

executing,locallywithHeroku/RunninganapplicationlocallywithHerokudeploying,withHeroku/DeployinganapplicationtoHeroku

applicationframeworkusing/UsinganapplicationframeworkExpress,using/GettingstartedwithExpress

arrayliteralnotationabout/FunctionalprogramminginJavaScript

asm.jsabout/Understandingasm.jsURL/Understandingasm.js

ASP.NETandJavaScript/JavaScriptandASP.NET.NETCore/Exploring.NETCoreintegration,withJavaScript/IntegrationwithJavaScriptserver-sideJavaScriptintegration/Server-sideJavaScriptintegrationwith.NET

assemblylanguageabout/Introducingatrueassemblylanguageforthewebasm.js/Understandingasm.jsWebAssembly/UnderstandingWebAssembly

assertionswriting,withChai/UsingChaiforassertions

asynchronouscodecallbackpattern,using/Usingthecallbackpatternforasynchronouscodewriting,promisesused/Writingcleanerasynchronouscodeusingpromisespromise-basedasynchronouscode,implementing/Implementingpromise-based

asynchronouscodeoperations,parallelizingwithpromises/Parallelisingoperationsusingpromises

asynchronousinterfacesconsuming/Consumingasynchronousinterfaces

AsynchronousModuleDefinition(AMD)/Writinguniversalmodulesasynchronousprogramming

about/Non-blockingasynchronousprogrammingpatterns

combining/Combiningasynchronousprogrammingpatternsasynchronoustests

writing,inMocha/TestinganExpressapplicationAtom

URL/Choosinganeditor

BBDD-styletests

writing,withMocha/WritingBDD-styletestswithMochastate,resetting/Resettingstatebetweentests

beforeEachhookabout/Resettingstatebetweentests

behavior-drivendevelopment(BDD)styleabout/WritingBDD-styletestswithMocha

bin/wwwfile/ExploringourExpressapplicationbinaryJSON(BSON)

about/IntroducingMongoDBbrowser

Node.jsmodules,using/UsingNode.jsmodulesinthebrowserBrowserify

URL/UsingNode.jsmodulesinthebrowser,ControllingBrowserify'soutputoutput,controlling/ControllingBrowserify'soutput

buildprocessautomating,withGulp/AutomatingthebuildprocesswithGulptests,executingwithGulp/RunningtestsusingGulp

Ccallbackfunction

about/Non-blockingcallbackpattern

using,asynchronouscode/Usingthecallbackpatternforasynchronouscodeexposing/Exposingthecallbackpatternasynchronousinterfaces,consuming/Consumingasynchronousinterfaces

Chaiused,forassertions/UsingChaiforassertionsURL/UsingChaiforassertions

chatroomimplementing,Socket.IOused/ImplementingachatroomwithSocket.IO

class-basedinheritanceabout/Class-basedinheritance

classesavoiding,inobject-orientedprogramming/Programmingwithoutclassesobjects,creatingwithnewkeyword/Creatingobjectswiththenewkeywordused,inobject-orientedprogramming/Programmingwithclassesclass-basedinheritance/Class-basedinheritance

client-sideJavaScriptreferencelink/IntegrationwithJavaScript

ClojureURL/Andbeyond...

codebaseorganizing/OrganizingyourcodebaseJavaScriptmodulesystems/JavaScriptmodulesystems

codecoveragestatisticsgathering/Gatheringcodecoveragestatistics

codestylechecking,withESLint/CheckingcodestylewithESLint

CoffeeScriptabout/CoffeeScript

collectionsabout/IntroducingMongoDB

CommonJSabout/JavaScriptmodulesystems

compile-to-JavaScriptlanguagesabout/Exploringcompile-to-JavaScriptlanguagesTypeScript/TypeScriptCoffeeScript/CoffeeScript

connect-owinprojectURL/Server-sideJavaScriptintegrationwith.NET

constkeyword

about/StrictmodeContinuousIntegration(CI)

about/SettingupanintegrationserverCookieChoices

URL/Decidingwhenthesessiongetssaved

DDart

URL/Andbeyond...databaseintegrationtests

executing,onTravisCI/RunningdatabaseintegrationtestsonTravisCIdataoperations

implementing/Implementingotherdataoperationsdata,listinginviews/Listingdatainviewsdeleterequest,issuingfromclient/IssuingadeleterequestfromtheclientExpressviews,splittingup/SplittingupExpressviewsusingpartials

DenialofService(DoS)attack/Runningautomatedclientsonthewebdependencies

managing,in.NETCore/Managingdependenciesin.NETCoredependencyinjection(DI)

inNode.js/DependencyinjectioninNode.jsdevelopmentdependency

about/WritingBDD-styletestswithMochadirectory-levelmodule

defining/Definingadirectory-levelmoduledocument-orientedDBMS

about/IntroducingMongoDBDocumentObjectModel(DOM)

about/WhatisNode.js?

EECMAScript

versioning/UnderstandingECMAScriptversioningECMAScript2015

exploring/ExploringECMAScript2015URL/ExploringECMAScript2015ES2015modules/UnderstandingES2015modulesgeneratorfunctions/Introducinggenerators

ECMAScript2016about/IntroducingECMAScript2016

Edge.jsURL/Server-sideJavaScriptintegrationwith.NET

Edge.jsprojectURL/Server-sideJavaScriptintegrationwith.NET

editorselecting/Choosinganeditor

encryptedenvironmentvariablesettingup/SettingencryptedTravisCIenvironmentvariablesRuby,installing/InstallingRubycreating/Creatinganencryptedenvironmentvariable

ES2015modulesabout/UnderstandingES2015modulesURL/UnderstandingES2015modulessyntaximprovements,using/UsingsyntaximprovementsfromES2015for...ofloop/Thefor...ofloopspreadoperator/Thespreadoperatorandrestparametersrestparameters/Thespreadoperatorandrestparametersassignment,destructuring/Destructuringassignment

ESLintcodestyle,checkingwith/CheckingcodestylewithESLintissues,fixingautomatically/AutomaticallyfixingissuesinESLintURL,forrules/AutomaticallyfixingissuesinESLintexecuting,fromGulp/RunningESLintfromGulp

event-drivenexecutionmodelabout/Event-driven

eventloopabout/Event-driven

executionmodel,Node.jsabout/UnderstandingtheNode.jsexecutionmodelnon-blocking/Non-blockingevent-driven/Event-drivensingle-threaded/Single-threaded

Express

using/GettingstartedwithExpressroutes/UnderstandingExpressroutesandviewsviews/UnderstandingExpressroutesandviewsnodemon,using/Usingnodemonforautomaticrestartsmodularapplications,creating/CreatingmodularapplicationswithExpressmiddleware/UnderstandingExpressmiddlewareMongoDB,usingwith/UsingMongoDBwithExpressSocket.IO,integrating/IntegratingSocket.IOwithExpress

Expressapplicationexploring/ExploringourExpressapplicationbootstrapping/BootstrappinganExpressapplicationtesting/TestinganExpressapplicationtests,simplifyingwithSuperAgent/SimplifyingtestsusingSuperAgent

Expressapplication,foldersnode_modules/ExploringourExpressapplicationpublic/ExploringourExpressapplicationroutes/ExploringourExpressapplicationviews/ExploringourExpressapplication

Expressmiddlewaremoduleimplementing/ImplementinganExpressmiddlewaremodule

Expresssessionsusing/UsingExpresssessionssessionsecret,specifying/Specifyingasessionsecretsession,saving/Decidingwhenthesessiongetssavedalternativesessionstores,using/Usingalternativesessionstoressessionmiddleware,using/Usingsessionmiddleware

Expressviewssplittingup,withpartials/SplittingupExpressviewsusingpartials

FFacebookapplication

URL/Addingotherloginprovidersfor...ofloop

about/Thefor...ofloopfull-stacktesting

withPhantomJS/Full-stacktestingwithPhantomJSfunctionalobject-orientedprogramming

about/Functionalobject-orientedprogrammingJavaScript/FunctionalprogramminginJavaScriptobject-orientedprogramming/Object-orientedprogramminginJavaScript

GGem

about/Creatinganencryptedenvironmentvariablegeneratorfunctions

about/Introducinggeneratorsreferencelink/Introducinggenerators

GitHubURL/SettingupapublicGitHubrepository,BuildingaprojectonTravisCI

governancemodelURL/AbriefhistoryofNode.js

Gulpbuildprocess,automating/AutomatingthebuildprocesswithGulptests,executing/RunningtestsusingGulpESLint,executingfrom/RunningESLintfromGulpintegrationtests,executing/RunningintegrationtestsfromGulp

HHangman

URL/Handlinguser-submitteddatauser-submitteddata,handling/Handlinguser-submitteddatacommunicating,viaAjax/CommunicatingviaAjaxdataoperations,implementing/Implementingotherdataoperations

hashesabout/StoringstructureddatainRedis

Herokuabout/WorkingwithHerokuURL/WorkingwithHerokuaccount,settingup/SettingupaHerokuaccountandtoolingapplication,executing/RunninganapplicationlocallywithHerokuapplication,deploying/DeployinganapplicationtoHerokuMongoDB,settingup/SettingupMongoDBRedis,settingup/SettingupRedis

Herokuconfigworkingwith/WorkingwithHerokulogs,config,andservices

Herokulogsworkingwith/WorkingwithHerokulogs,config,andservices

Herokuservicesworkingwith/WorkingwithHerokulogs,config,andservices

herokutoolbeltURL/SettingupaHerokuaccountandtooling

higher-orderfunctionsabout/FunctionalprogramminginJavaScript

IImmediately-InvokedFunctionExpression(IIFE)/Supportingthebrowserenvironmentintegrationserver

about/Settingupanintegrationserversettingup/SettingupanintegrationserverpublicGitHubrepository,settingup/SettingupapublicGitHubrepositoryproject,buildingonTravisCI/BuildingaprojectonTravisCI

integrationtestsexecuting,fromGulp/RunningintegrationtestsfromGulp

io.jsabout/AbriefhistoryofNode.js

IsomorphicJavaScriptURL/IsomorphicJavaScript

JJasmine

about/WritingBDD-styletestswithMochaJavaScript

about/WhyJavaScript?,JavaScriptcanvas/Aclearcanvasfunctions/Functionalnaturefuture/Abrightfuturefunctionalprogramming/FunctionalprogramminginJavaScriptscopes/UnderstandingscopesinJavaScriptobject-orientedprogramming/Object-orientedprogramminginJavaScriptexploring/GoingbeyondJavaScriptreferencelink/GoingbeyondJavaScriptcompile-to-JavaScriptlanguages/Exploringcompile-to-JavaScriptlanguagesassemblylanguage/IntroducingatrueassemblylanguageforthewebandASP.NET/JavaScriptandASP.NETASP.NETintegration/IntegrationwithJavaScript

JavaScriptmodulesystemsabout/JavaScriptmodulesystems

JavaScriptprimitivetypesabout/JavaScriptprimitivetypes

JavaScripttypesabout/IntroducingJavaScripttypesprimitivetypes/JavaScriptprimitivetypes

Kkey-valuedatastore

about/IntroducingRedis

LLanguage-IntegratedQuery(LINQ)

about/FunctionalprogramminginJavaScriptletkeyword

about/Strictmodelists

about/StoringstructureddatainRedisloginproviders

adding/Addingotherloginproviderslong-polling/Understandingoptionsforreal-timecommunicationlong-termsupport(LTS)

about/IntroducingtheNode.jsLTSscheduleLTSschedule

about/IntroducingtheNode.jsLTSscheduleURL/IntroducingtheNode.jsLTSschedule

MMap-Reduce

about/UsingtheMongoDBshellURL/UsingtheMongoDBshell

middlewareabout/BuildingwebapplicationsinASP.NETCoreURL/BuildingwebapplicationsinASP.NETCore

middleware,Expressabout/UnderstandingExpressmiddlewareerrorhandling,implementing/Implementingerrorhandlingusing/UsingExpressmiddleware

MochaBDD-styletests,writing/WritingBDD-styletestswithMochaabout/WritingBDD-styletestswithMochaasynchronoustests,writing/TestinganExpressapplication

Mockgooseabout/Providingdependencies

mocksabout/CreatingtestdoublesusingSinon.JS

modelabout/PersistingobjectswithMongoose

modularapplicationscreating,withExpress/CreatingmodularapplicationswithExpress

modulecountsreferencelink/IntroducingtheNode.jsecosystem

modulesabout/Organizingyourcodebasecreating/CreatingmodulesinNode.jsdeclaring,withname/Declaringamodulewithanameanditsownscopedeclaring,withscope/Declaringamodulewithanameanditsownscopefunctionality,defining/Definingfunctionalityprovidedbythemoduleimporting,intoanotherscript/Importingamoduleintoanotherscriptdirectory-levelmodule,defining/Definingadirectory-levelmoduleExpressmiddlewaremodule,implementing/ImplementinganExpressmiddlewaremodule

ModulusURL/Furtherresources

MongoDBabout/IntroducingMongoDBadvantages/WhychooseMongoDB?objectmodelling/ObjectmodelingJavaScript/JavaScriptscalability/Scalability

URL/GettingstartedwithMongoDBURL,forinstallation/GettingstartedwithMongoDBdirectory,creating/GettingstartedwithMongoDBshell,using/UsingtheMongoDBshellURL,fordocumentation/UsingtheMongoDBshellusing,withExpress/UsingMongoDBwithExpressobjects,persistingwithMongoose/PersistingobjectswithMongoosepersistencecode,isolating/Isolatingpersistencecodedependencyinjection(DI),inNode.js/DependencyinjectioninNode.jsdependencies,providing/Providingdependenciesdatabaseintegrationtests,executing/RunningdatabaseintegrationtestsonTravisCIsettingup/SettingupMongoDB

Mongooseabout/UsingMongoDBwithExpressobjects,persisting/PersistingobjectswithMongoose

MustacheURL/UnderstandingExpressroutesandviews

N.NETCore

about/Exploring.NETCoreprojectstructure,defining/Definingprojectstructurein.NETCoredependencies,managing/Managingdependenciesin.NETCorewebapplications,building/BuildingwebapplicationsinASP.NETCore

N4JSURL/TypeScript

namespacesused,fororganizingSocket.IOapplications/OrganizingSocket.IOapplicationsusingnamespaces

newkeywordused,forcreatingobjects/Creatingobjectswiththenewkeyword

Node.jsabout/WhatisNode.js?ecosystem/IntroducingtheNode.jsecosystemusage/WhentouseNode.jswebapplications,writing/Writingwebapplicationsusecases/Identifyingotherusecasesneedfor/Whynow?URL/InstallingandrunningNode.jsinstalling/InstallingandrunningNode.jsrunning/InstallingandrunningNode.jsdependencyinjection(DI)/DependencyinjectioninNode.jsRedis,using/UsingRedisfromNode.jsandRequireJS,comparing/ComparingNode.jsandRequireJSversioning/UnderstandingNode.jsversioninghistory/AbriefhistoryofNode.jsLTSschedule/IntroducingtheNode.jsLTSschedule

Node.jsmodulesusing,inbrowser/UsingNode.jsmodulesinthebrowserBrowserifyoutput,controlling/ControllingBrowserify'soutput

nodemonusing/Usingnodemonforautomaticrestarts

non-blockingexecutionmodelabout/Non-blocking

npmabout/IntroducingtheNode.jsecosystempackage,publishing/Publishingapackagetonpm

npmcommandlinetoolabout/IntroducingtheNode.jsecosystem

npmpackageswriting/Writingnpmpackages

defining/Definingannpmpackagestandalonetool,releasing/Releasingastandalonetooltonpm

npmregistryabout/IntroducingtheNode.jsecosystem

NuGetpackagemanagerabout/Managingdependenciesin.NETCore

Nulltypeabout/JavaScriptprimitivetypes

numberabout/JavaScriptprimitivetypes

Oobject

about/Object-orientedprogramminginJavaScriptobject-oriented(OO)

about/WhyJavaScript?object-orientedprogramming

inJavaScript/Object-orientedprogramminginJavaScriptwithoutclasses/Programmingwithoutclasseswithclasses/Programmingwithclasses

Object-RelationalMapper(ORM)about/Objectmodeling

objectmodellingabout/Objectmodeling

objectscreating,withnewkeyword/Creatingobjectswiththenewkeyword

OmniSharpprojectURL/Definingprojectstructurein.NETCore

OpenIDConnectURL/Addingotherloginproviders

OWINabout/BuildingwebapplicationsinASP.NETCoreURL/BuildingwebapplicationsinASP.NETCore

Ppackage

publishing,tonpm/Publishingapackagetonpmautomatedclients,runningonweb/Runningautomatedclientsonthewebstandalonetool,releasingtonpm/Releasingastandalonetooltonpm

package.jsonfile/ExploringourExpressapplicationPassport

about/IntroducingPassportauthenticationstrategy,selecting/Choosinganauthenticationstrategythird-partyauthentication/Understandingthird-partyauthenticationconfiguring/ConfiguringPassportconfiguring,withpersistence/ConfiguringPassportwithpersistenceintegrationtesting/IntegrationtestingwithPassport

PhantomJSfull-stacktesting/Full-stacktestingwithPhantomJSabout/Full-stacktestingwithPhantomJS

pipelinestagesabout/UsingtheMongoDBshell

Platform-as-a-Service(PaaS)/FurtherresourcesProcfile

about/RunninganapplicationlocallywithHerokupromise-basedasynchronouscode

implementing/Implementingpromise-basedasynchronouscodeconsuming/Consumingthepromisepattern

Promise.coroutinemethodURL/Introducinggenerators

promisesused,forwritingasynchronouscode/Writingcleanerasynchronouscodeusingpromisesabout/Writingcleanerasynchronouscodeusingpromisespendingstates/Writingcleanerasynchronouscodeusingpromisesfulfilledstates/Writingcleanerasynchronouscodeusingpromisesrejectedstates/Writingcleanerasynchronouscodeusingpromisesused,forparallelizingoperations/Parallelisingoperationsusingpromises

Promises/A+URL/Combiningasynchronousprogrammingpatterns

prototypalinheritanceabout/Programmingwithoutclasses

prototypeabout/Programmingwithoutclasses

publicGitHubrepositorysettingup/SettingupapublicGitHubrepository

RRead-Eval-PrintLoop(REPL)

about/WhatisNode.js?real-timecommunication

options/Understandingoptionsforreal-timecommunicationreal-timeNode.jsapplications

scaling/Scalingreal-timeNode.jsapplicationsRedis,usingasbackend/UsingRedisasabackend

Redisabout/IntroducingRedisadvantages/WhyuseRedis?installing/InstallingRedisURL/InstallingRedisused,askey-valuestore/UsingRedisasakey-valuestorestructureddata,storing/StoringstructureddatainRedislists/StoringstructureddatainRedishashes/StoringstructureddatainRedissets/StoringstructureddatainRedissortedsets/StoringstructureddatainRedisuserrankingsystem,building/BuildingauserrankingsystemwithRedisusing,fromNode.js/UsingRedisfromNode.jssettingup/SettingupRedisuserdata,persisting/PersistinguserdatawithRedis

redis-jsused,fortesting/Testingwithredis-js

relationalpropertyabout/IntroducingMongoDB

RequireJSandNode.js,comparing/ComparingNode.jsandRequireJS

restparametersabout/Thespreadoperatorandrestparameters

robots.txtfileURL/Runningautomatedclientsontheweb

roomsused,forpartitioningSocket.IOclients/PartitioningSocket.IOclientsusingrooms

routes,Expressabout/UnderstandingExpressroutesandviews

Rubyinstalling/InstallingRubyURL/InstallingRuby

RubyInstallerURL/InstallingRuby

Sschema

about/PersistingobjectswithMongoosescopes,JavaScript

about/UnderstandingscopesinJavaScriptglobal/UnderstandingscopesinJavaScriptfunctional/UnderstandingscopesinJavaScriptstrictmode/Strictmode

securityabout/Anoteonsecurityreferences/Anoteonsecurity

SemanticVersioning2.0.0URL/AbriefhistoryofNode.js

server-sideJavaScriptintegrationabout/Server-sideJavaScriptintegrationwith.NET

sessionmiddlewareusing/Usingsessionmiddleware

setsabout/StoringstructureddatainRedis

single-threadedexecutionmodelabout/Single-threaded

Sinon.JSused,forcreatingtestdoubles/CreatingtestdoublesusingSinon.JSURL/CreatingtestdoublesusingSinon.JS

socialloginimplementing/ImplementingsocialloginTwitterapplication,settingup/SettingupaTwitterapplicationPassport,configuring/ConfiguringPassportuserdata,persistingwithRedis/PersistinguserdatawithRedisPassport,configuringwithpersistence/ConfiguringPassportwithpersistencefunctionality,hidingfromunauthenticatedusers/Hidingfunctionalityfromunauthenticatedusersintegrationtesting,withPassport/IntegrationtestingwithPassport

Socket.IOabout/IntroducingSocket.IOchatroom,implementing/ImplementingachatroomwithSocket.IOintegrating,withExpress/IntegratingSocket.IOwithExpressmessages,directing/DirectingSocket.IOmessagesapplications,testing/TestingSocket.IOapplications

Socket.IOapplications,organizingabout/OrganizingSocket.IOapplicationsreal-timeupdates,exposingtomodel/Exposingreal-timeupdatestothemodelnamespacesused/OrganizingSocket.IOapplicationsusingnamespaces

Socket.IOclientspartitioning,roomsused/PartitioningSocket.IOclientsusingroomssortedsets

about/StoringstructureddatainRedisspies

about/CreatingtestdoublesusingSinon.JSspreadoperator

about/Thespreadoperatorandrestparametersspy

about/Creatingtestdoublesstrictmock

about/CreatingtestdoublesusingSinon.JSstrictmode

about/Strictmodestrings

about/JavaScriptprimitivetypesStrings

about/UsingRedisasakey-valuestorestubs

about/CreatingtestdoublesusingSinon.JSSuperAgent

tests,simplifyingwith/SimplifyingtestsusingSuperAgentURL/SimplifyingtestsusingSuperAgent

SuperTestURL/SimplifyingtestsusingSuperAgent

Ttableofcontents(ToC)/Writingnpmpackagestemplatingengines

about/GettingstartedwithExpresstestdoubles

creating/Creatingtestdoublesabout/Creatingtestdoublescreating,Sinon.JSused/CreatingtestdoublesusingSinon.JS

testswriting/WritingasimpletestinNode.jscodebase,structuring/Structuringthecodebasefortestssimplifying,withSuperAgent/SimplifyingtestsusingSuperAgent

TravisCIURL/Settingupanintegrationserver,BuildingaprojectonTravisCIusing/Settingupanintegrationserverproject,building/BuildingaprojectonTravisCIdatabaseintegrationtests,executing/RunningdatabaseintegrationtestsonTravisCIused,fordeploying/DeployingfromTravisCIencryptedenvironmentvariable,settingup/SettingencryptedTravisCIenvironmentvariables

Twelve-FactorAppURL/Furtherresources

TwitterURL/SettingupaTwitterapplication

Twitterapplicationsettingup/SettingupaTwitterapplication

typedefinitionsreferencelink/TypeScript

TypeScriptabout/TypeScriptURL/TypeScript

UUndefinedtype

about/JavaScriptprimitivetypesuniversalmodules

writing/WritinguniversalmodulesNode.jsandRequireJS,comparing/ComparingNode.jsandRequireJSbrowserenvironment,supporting/SupportingthebrowserenvironmentAMDmodules,usingwithRequireJS/UsingAMDmoduleswithRequireJSisomorphicJavaScript/IsomorphicJavaScript

user-submitteddatahandling/Handlinguser-submitteddata

userrankingsystembuilding,withRedis/BuildingauserrankingsystemwithRedisuserrankings,implementing/ImplementinguserrankingswithRedisusersservice,using/Makinguseoftheusersservice

usersallowing,tologout/Allowinguserstologout

Vvariablehoisting

about/UnderstandingscopesinJavaScriptverifycallback

about/ConfiguringPassportviews,Express

about/UnderstandingExpressroutesandviewsVisualStudioCode

URL/Choosinganeditor

Wwebapplications

writing,withNode.js/Writingwebapplicationsbuilding,in.NETCore/BuildingwebapplicationsinASP.NETCore

WebAssemblyURL/UnderstandingWebAssemblyabout/UnderstandingWebAssembly

WerckerURL/Furtherresources