Create Restful Web Application With Node.js Express Framework
Table of Contents · Express is a Node.js Web Framework. Node.js is an amazing tool for building...
Transcript of Table of Contents · Express is a Node.js Web Framework. Node.js is an amazing tool for building...
TableofContentsTheExpressHandbook
Expressoverview
Requestparameters
Sendingaresponse
SendingaJSONresponse
ManageCookies
WorkwithHTTPheaders
Redirects
Routing
CORS
Templating
ThePugGuide
Middleware
Servingstaticfiles
Sendfiles
Sessions
Validatinginput
Sanitizinginput
Handlingforms
Fileuploadsinforms
AnExpressHTTPSserverwithaself-signedcertificate
SetupLet'sEncryptforExpress
2
TheExpressHandbookTheExpressHandbookfollowsthe80/20rule:learnin20%ofthetimethe80%ofatopic.
Ifindthisapproachgivesawell-roundedoverview.ThisbookdoesnottrytocovereverythingunderthesunrelatedtoExpress.Ifyouthinksomespecifictopicshouldbeincluded,tellme.
YoucanreachmeonTwitter@flaviocopes.
Ihopethecontentsofthisbookwillhelpyouachievewhatyouwant:learnthebasicsExpress.
ThisbookiswrittenbyFlavio.Ipublishwebdevelopmenttutorialseverydayonmywebsiteflaviocopes.com.
Enjoy!
TheExpressHandbook
3
ExpressoverviewExpressisaNode.jsWebFramework.Node.jsisanamazingtoolforbuildingnetworkingservicesandapplications.ExpressbuildsontopofitsfeaturestoprovideeasytousefunctionalitythatsatisfytheneedsoftheWebServerusecase.
ExpressisaNode.jsWebFramework.
Node.jsisanamazingtoolforbuildingnetworkingservicesandapplications.
ExpressbuildsontopofitsfeaturestoprovideeasytousefunctionalitythatsatisfytheneedsoftheWebServerusecase.
It'sOpenSource,free,easytoextend,veryperformant,andhaslotsandlotsofpre-builtpackagesyoucanjustdropinanduse,toperformallkindofthings.
InstallationYoucaninstallExpressintoanyprojectwithnpm:
Expressoverview
4
npminstallexpress--save
orYarn:
yarnaddexpress
Bothcommandswillalsoworkinanemptydirectory,tostartupyourprojectfromscratch,although npmdoesnotcreatea package.jsonfileatall,andYarncreatesabasicone.
Justrun npminitor yarninitifyou'restartinganewprojectfromscratch.
HelloWorldWe'rereadytocreateourfirstExpressWebServer.
Hereissomecode:
constexpress=require('express')
constapp=express()
app.get('/',(req,res)=>res.send('HelloWorld!'))
app.listen(3000,()=>console.log('Serverready'))
Savethistoan index.jsfileinyourprojectrootfolder,andstarttheserverusing
nodeindex.js
Youcanopenthebrowsertoport3000onlocalhostandyoushouldseethe HelloWorld!message.
LearnthebasicsofExpressbyunderstandingtheHelloWorldcodeThose4linesofcodedoalotbehindthescenes.
First,weimportthe expresspackagetothe expressvalue.
Weinstantiateanapplicationbycallingits app()method.
Oncewehavetheapplicationobject,wetellittolistenforGETrequestsonthe /path,usingthe get()method.
Expressoverview
5
ThereisamethodforeveryHTTPverb: get(), post(), put(), delete(), patch():
app.get('/',(req,res)=>{/**/})
app.post('/',(req,res)=>{/**/})
app.put('/',(req,res)=>{/**/})
app.delete('/',(req,res)=>{/**/})
app.patch('/',(req,res)=>{/**/})
Thosemethodsacceptacallbackfunction,whichiscalledwhenarequestisstarted,andweneedtohandleit.
Wepassinanarrowfunction:
(req,res)=>res.send('HelloWorld!')
Expresssendsustwoobjectsinthiscallback,whichwecalled reqand res,thatrepresenttheRequestandtheResponseobjects.
RequestistheHTTPrequest.Itcangiveusalltheinfoaboutthat,includingtherequestparameters,theheaders,thebodyoftherequest,andmore.
ResponseistheHTTPresponseobjectthatwe'llsendtotheclient.
Whatwedointhiscallbackistosendthe'HelloWorld!'stringtotheclient,usingtheResponse.send()method.
Thismethodsetsthatstringasthebody,anditclosestheconnection.
Thelastlineoftheexampleactuallystartstheserver,andtellsittolistenonport 3000.Wepassinacallbackthatiscalledwhentheserverisreadytoacceptnewrequests.
Expressoverview
6
RequestparametersAhandyreferencetoalltherequestobjectpropertiesandhowtousethem
RequestparametersImentionedhowtheRequestobjectholdsalltheHTTPrequestinformation.
Thesearethemainpropertiesyou'lllikelyuse:
Property Description
.app holdsareferencetotheExpressappobject
.baseUrl thebasepathonwhichtheappresponds
.body containsthedatasubmittedintherequestbody(mustbeparsedandpopulatedmanuallybeforeyoucanaccessit)
.cookies containsthecookiessentbytherequest(needsthe cookie-parsermiddleware)
.hostname theserverhostname
.ip theserverIP
.method theHTTPmethodused
.params theroutenamedparameters
.path theURLpath
.protocol therequestprotocol
.query anobjectcontainingallthequerystringsusedintherequest
.secure trueiftherequestissecure(usesHTTPS)
.signedCookies containsthesignedcookiessentbytherequest(needsthe cookie-parsermiddleware)
.xhr trueiftherequestisanXMLHttpRequest
HowtoretrievetheGETquerystringparametersusingExpressThequerystringisthepartthatcomesaftertheURLpath,andstartswithanexclamationmark ?.
Requestparameters
7
Example:
?name=flavio
Multiplequeryparameterscanbeaddedusing &:
?name=flavio&age=35
HowdoyougetthosequerystringvaluesinExpress?
Expressmakesitveryeasybypopulatingthe Request.queryobjectforus:
constexpress=require('express')
constapp=express()
app.get('/',(req,res)=>{
console.log(req.query)
})
app.listen(8080)
Thisobjectisfilledwithapropertyforeachqueryparameter.
Iftherearenoqueryparams,it'sanemptyobject.
Thismakesiteasytoiterateonitusingthefor...inloop:
for(constkeyinreq.query){
console.log(key,req.query[key])
}
Thiswillprintthequerypropertykeyandthevalue.
Youcanaccesssinglepropertiesaswell:
req.query.name//flavio
req.query.age//35
HowtoretrievethePOSTquerystringparametersusingExpressPOSTqueryparametersaresentbyHTTPclientsforexamplebyforms,orwhenperformingaPOSTrequestsendingdata.
Requestparameters
8
Howcanyouaccessthisdata?
IfthedatawassentasJSON,using Content-Type:application/json,youwillusetheexpress.json()middleware:
constexpress=require('express')
constapp=express()
app.use(express.json())
IfthedatawassentasJSON,using Content-Type:application/x-www-form-urlencoded,youwillusethe express.urlencoded()middleware:
constexpress=require('express')
constapp=express()
app.use(express.urlencoded())
Inbothcasesyoucanaccessthedatabyreferencingitfrom Request.body:
app.post('/form',(req,res)=>{
constname=req.body.name
})
Note:olderExpressversionsrequiredtheuseofthe body-parsermoduletoprocessPOSTdata.ThisisnolongerthecaseasofExpress4.16(releasedinSeptember2017)andlaterversions.
Requestparameters
9
SendingaresponseHowtosendaresponsebacktotheclientusingExpress
IntheHelloWorldexampleweusedthe Response.send()methodtosendasimplestringasaresponse,andtoclosetheconnection:
(req,res)=>res.send('HelloWorld!')
Ifyoupassinastring,itsetsthe Content-Typeheaderto text/html.
ifyoupassinanobjectoranarray,itsetsthe application/json Content-Typeheader,andparsesthatparameterintoJSON.
send()automaticallysetsthe Content-LengthHTTPresponseheader.
send()alsoautomaticallyclosestheconnection.
Useend()tosendanemptyresponse
Analternativewaytosendtheresponse,withoutanybody,it'sbyusingthe Response.end()method:
res.end()
SettheHTTPresponsestatus
Usethe Response.status():
res.status(404).end()
or
res.status(404).send('Filenotfound')
sendStatus()isashortcut:
res.sendStatus(200)
//===res.status(200).send('OK')
res.sendStatus(403)
//===res.status(403).send('Forbidden')
Sendingaresponse
10
res.sendStatus(404)
//===res.status(404).send('NotFound')
res.sendStatus(500)
//===res.status(500).send('InternalServerError')
Sendingaresponse
11
SendingaJSONresponseHowtoserveJSONdatausingtheNode.jsExpresslibrary
WhenyoulistenforconnectionsonarouteinExpress,thecallbackfunctionwillbeinvokedoneverynetworkcallwithaRequestobjectinstanceandaResponseobjectinstance.
Example:
app.get('/',(req,res)=>res.send('HelloWorld!'))
Hereweusedthe Response.send()method,whichacceptsanystring.
YoucansendJSONtotheclientbyusing Response.json(),ausefulmethod.
Itacceptsanobjectorarray,andconvertsittoJSONbeforesendingit:
res.json({username:'Flavio'})
SendingaJSONresponse
12
ManageCookiesHowtousethe`Response.cookie()`methodtomanipulateyourcookies
Usethe Response.cookie()methodtomanipulateyourcookies.
Examples:
res.cookie('username','Flavio')
Thismethodacceptsathirdparameterwhichcontainsvariousoptions:
res.cookie('username','Flavio',{domain:'.flaviocopes.com',path:'/administrator',sec
ure:true})
res.cookie('username','Flavio',{expires:newDate(Date.now()+900000),httpOnly:true
})
Themostusefulparametersyoucansetare:
Value Description
domain thecookiedomainname
expiressetthecookieexpirationdate.Ifmissing,or0,thecookieisasessioncookie
httpOnly setthecookietobeaccessibleonlybythewebserver.SeeHttpOnly
maxAge settheexpirytimerelativetothecurrenttime,expressedinmilliseconds
path thecookiepath.Defaultsto/
secure MarksthecookieHTTPSonly
signed setthecookietobesigned
sameSite Valueof SameSite
Acookiecanbeclearedwith
res.clearCookie('username')
ManageCookies
13
WorkwithHTTPheadersLearnhowtoaccessandchangeHTTPheadersusingExpress
AccessHTTPheadersvaluesfromarequestYoucanaccessalltheHTTPheadersusingthe Request.headersproperty:
app.get('/',(req,res)=>{
console.log(req.headers)
})
Usethe Request.header()methodtoaccessoneindividualrequestheadervalue:
app.get('/',(req,res)=>{
req.header('User-Agent')
})
ChangeanyHTTPheadervalueofaresponseYoucanchangeanyHTTPheadervalueusing Response.set():
res.set('Content-Type','text/html')
ThereisashortcutfortheContent-Typeheaderhowever:
res.type('.html')
//=>'text/html'
res.type('html')
//=>'text/html'
res.type('json')
//=>'application/json'
res.type('application/json')
//=>'application/json'
res.type('png')
//=>image/png:
WorkwithHTTPheaders
14
WorkwithHTTPheaders
15
RedirectsHowtoredirecttootherpagesserver-side
RedirectsarecommoninWebDevelopment.YoucancreatearedirectusingtheResponse.redirect()method:
res.redirect('/go-there')
Thiscreatesa302redirect.
A301redirectismadeinthisway:
res.redirect(301,'/go-there')
Youcanspecifyanabsolutepath( /go-there),anabsoluteurl( https://anothersite.com),arelativepath( go-there)orusethe ..togobackonelevel:
res.redirect('../go-there')
res.redirect('..')
YoucanalsoredirectbacktotheRefererHTTPheadervalue(defaultingto /ifnotset)using
res.redirect('back')
Redirects
16
RoutingRoutingistheprocessofdeterminingwhatshouldhappenwhenaURLiscalled,oralsowhichpartsoftheapplicationshouldhandleaspecificincomingrequest.
RoutingistheprocessofdeterminingwhatshouldhappenwhenaURLiscalled,oralsowhichpartsoftheapplicationshouldhandleaspecificincomingrequest.
IntheHelloWorldexampleweusedthiscode
app.get('/',(req,res)=>{/**/})
ThiscreatesaroutethatmapsaccessingtherootdomainURL /usingtheHTTPGETmethodtotheresponsewewanttoprovide.
Namedparameters
Whatifwewanttolistenforcustomrequests,maybewewanttocreateaservicethatacceptsastring,andreturnsthatuppercase,andwedon'twanttheparametertobesentasaquerystring,butpartoftheURL.Weusenamedparameters:
app.get('/uppercase/:theValue',(req,res)=>res.send(req.params.theValue.toUpperCase()))
Ifwesendarequestto /uppercase/test,we'llget TESTinthebodyoftheresponse.
YoucanusemultiplenamedparametersinthesameURL,andtheywillallbestoredinreq.params.
Usearegularexpressiontomatchapath
Youcanuseregularexpressionstomatchmultiplepathswithonestatement:
app.get(/post/,(req,res)=>{/**/})
willmatch /post, /post/first, /thepost, /posting/something,andsoon.
Routing
17
CORSHowtoallowcrosssiterequestsbysettingupCORS
AJavaScriptapplicationrunninginthebrowsercanusuallyonlyaccessHTTPresourcesonthesamedomain(origin)thatservesit.
Loadingimagesorscripts/stylesalwaysworks,butXHRandFetchcallstoanotherserverwillfail,unlessthatserverimplementsawaytoallowthatconnection.
ThiswayiscalledCORS,Cross-OriginResourceSharing.
AlsoloadingWebFontsusing @font-facehassame-originpolicybydefault,andotherlesspopularthings(likeWebGLtexturesand drawImageresourcesloadedintheCanvasAPI).
OneveryimportantthingthatneedsCORSisESModules,recentlyintroducedinmodernbrowsers.
Ifyoudon'tsetupaCORSpolicyontheserverthatallowstoserve3rdpartorigins,therequestwillfail.
Fetchexample:
XHRexample:
CORS
18
ACross-Originresourcefailsifit's:
toadifferentdomaintoadifferentsubdomaintoadifferentporttoadifferentprotocol
andit'sthereforyoursecurity,topreventmalicioususerstoexploittheWebPlatform.
Butifyoucontrolboththeserverandtheclient,youhaveallthegoodreasonstoallowthemtotalktoeachother.
How?
Itdependsonyourserver-sidestack.
BrowsersupportPrettygood(basicallyallexceptIE<10):
ExamplewithExpressIfyouareusingNode.jsandExpressasaframework,usetheCORSmiddlewarepackage.
Here'sasimpleimplementationofanExpressNode.jsserver:
constexpress=require('express')
constapp=express()
CORS
19
app.get('/without-cors',(req,res,next)=>{
res.json({msg:'ۘ noCORS,noparty!'})
})
constserver=app.listen(3000,()=>{
console.log('Listeningonport%s',server.address().port)
})
Ifyouhit /without-corswithafetchrequestfromadifferentorigin,it'sgoingtoraisetheCORSissue.
Allyouneedtodotomakethingsworkoutistorequirethe corspackagelinkedabove,andpassitinasamiddlewarefunctiontoanendpointrequesthandler:
constexpress=require('express')
constcors=require('cors')
constapp=express()
app.get('/with-cors',cors(),(req,res,next)=>{
res.json({msg:'WHOAHwithCORSitworks!ث ʦ '})
})
/*therestoftheapp*/
ImadeasimpleGlitchexample.Hereistheclientworking,andhere'sitscode:https://glitch.com/edit/#!/flavio-cors-client.
ThisistheNode.jsExpressserver:https://glitch.com/edit/#!/flaviocopes-cors-example-express
NotehowtherequestthatfailsbecauseitdoesnothandletheCORSheadingscorrectlyisstillreceived,asyoucanseeintheNetworkpanel,whereyoufindthemessagetheserversent:
CORS
20
AllowonlyspecificoriginsThisexamplehasaproblemhowever:ANYrequestwillbeacceptedbytheserverascross-origin.
AsyoucanseeintheNetworkpanel,therequestthatpassedhasaresponseheader access-control-allow-origin:*:
Youneedtoconfiguretheservertoonlyallowoneorigintoserve,andblockalltheothers.
Usingthesame corsNodelibrary,here'showyouwoulddoit:
constcors=require('cors')
constcorsOptions={
origin:'https://yourdomain.com'
}
app.get('/products/:id',cors(corsOptions),(req,res,next)=>{
//...
})
Youcanservemoreaswell:
constwhitelist=['http://example1.com','http://example2.com']
constcorsOptions={
origin:function(origin,callback){
if(whitelist.indexOf(origin)!==-1){
callback(null,true)
}else{
callback(newError('NotallowedbyCORS'))
}
}
}
CORS
21
PreflightTherearesomerequeststhatarehandledina"simple"way.All GETrequestsbelongtothisgroup.
Alsosome POSTand HEADrequestsdoaswell.
POSTrequestsarealsointhisgroup,iftheysatisfytherequirementofusingaContent-Typeof
application/x-www-form-urlencoded
multipart/form-data
text/plain
Allotherrequestsmustrunthroughapre-approvalphase,calledpreflight.Thebrowserdoesthistodetermineifithasthepermissiontoperformanaction,byissuingan OPTIONSrequest.
Apreflightrequestcontainsafewheadersthattheserverwillusetocheckpermissions(irrelevantfieldsomitted):
OPTIONS/the/resource/you/request
Access-Control-Request-Method:POST
Access-Control-Request-Headers:origin,x-requested-with,accept
Origin:https://your-origin.com
Theserverwillrespondwithsomethinglikethis(irrelevantfieldsomitted):
HTTP/1.1200OK
Access-Control-Allow-Origin:https://your-origin.com
Access-Control-Allow-Methods:POST,GET,OPTIONS,DELETE
WecheckedforPOST,buttheservertellsuswecanalsoissueotherHTTPrequesttypesforthatparticularresource.
FollowingtheNode.jsExpressexampleabove,theservermustalsohandletheOPTIONSrequest:
varexpress=require('express')
varcors=require('cors')
varapp=express()
//allowOPTIONSonjustoneresource
app.options('/the/resource/you/request',cors())
//allowOPTIONSonallresources
app.options('*',cors())
CORS
22
CORS
23
TemplatingExpressiscapableofhandlingserver-sidetemplateengines.Templateenginesallowustoadddatatoaview,andgenerateHTMLdynamically.
Expressiscapableofhandlingserver-sidetemplateengines.
Templateenginesallowustoadddatatoaview,andgenerateHTMLdynamically.
ExpressusesJadeasthedefault.JadeistheoldversionofPug,specificallyPug1.0.
ThenamewaschangedfromJadetoPugduetoatrademarkissuein2016,whentheprojectreleasedversion2.YoucanstilluseJade,akaPug1.0,butgoingforward,it'sbesttousePug2.0
AlthoughthelastversionofJadeis3yearsold(atthetimeofwriting,summer2018),it'sstillthedefaultinExpressforbackwardcompatibilityreasons.
Inanynewproject,youshouldusePugoranotherengineofyourchoice.TheofficialsiteofPugishttps://pugjs.org/.
Youcanusemanydifferenttemplateengines,includingPug,Handlebars,Mustache,EJSandmore.
UsingPugTousePugwemustfirstinstallit:
npminstallpug
andwheninitializingtheExpressapp,weneedtosetit:
constexpress=require('express')
constapp=express()
app.set('viewengine','pug')
Wecannowstartwritingourtemplatesin .pugfiles.
Createanaboutview:
app.get('/about',(req,res)=>{
res.render('about')
})
Templating
24
andthetemplatein views/about.pug:
pHellofromFlavio
Thistemplatewillcreatea ptagwiththecontent HellofromFlavio.
Youcaninterpolateavariableusing
app.get('/about',(req,res)=>{
res.render('about',{name:'Flavio'})
})
pHellofrom#{name}
ThisisaveryshortintroductiontoPug,inthecontextofusingitwithExpress.LookatthePugguideformoreinformationonhowtousePug.
IfyouareusedtotemplateenginesthatuseHTMLandinterpolatevariables,likeHandlebars(describednext),youmightrunintoissues,especiallywhenyouneedtoconvertexistingHTMLtoPug.ThisonlineconverterfromHTMLtoJade(whichisverysimilar,butalittledifferentthanPug)willbeagreathelp:https://jsonformatter.org/html-to-jade
AlsoseethedifferencesbetweenJadeandPug
UsingHandlebarsLet'stryanduseHandlebarsinsteadofPug.
Youcaninstallitusing npminstallhbs.
Putan about.hbstemplatefileinthe views/folder:
Hellofrom{{name}}
andthenusethisExpressconfigurationtoserveiton /about:
constexpress=require('express')
constapp=express()
consthbs=require('hbs')
app.set('viewengine','hbs')
app.set('views',path.join(__dirname,'views'))
app.get('/about',(req,res)=>{
Templating
25
res.render('about',{name:'Flavio'})
})
app.listen(3000,()=>console.log('Serverready'))
YoucanalsorenderaReactapplicationserver-side,usingthe express-react-viewspackage.
Startwith npminstallexpress-react-viewsreactreact-dom.
Nowinsteadofrequiring hbswerequire express-react-viewsandusethatastheengine,using jsxfiles:
constexpress=require('express')
constapp=express()
app.set('viewengine','jsx')
app.engine('jsx',require('express-react-views').createEngine())
app.get('/about',(req,res)=>{
res.render('about',{name:'Flavio'})
})
app.listen(3000,()=>console.log('Serverready'))
Justputan about.jsxfilein views/,andcalling /aboutshouldpresentyouan"HellofromFlavio"string:
constReact=require('react')
classHelloMessageextendsReact.Component{
render(){
return<div>Hellofrom{this.props.name}</div>
}
}
module.exports=HelloMessage
Templating
26
ThePugGuideHowtousethePugtemplatingengine
IntroductiontoPugHowdoesPuglooklikeInstallPugSetupPugtobethetemplateengineinExpressYourfirstPugtemplateInterpolatingvariablesinPugInterpolateafunctionreturnvalueAddingidandclassattributestoelementsSetthedoctypeMetatagsAddingscriptsandstylesInlinescriptsLoopsConditionalsSetvariablesIncrementingvariablesAssigningvariablestoelementvaluesIteratingovervariablesIncludingotherPugfilesDefiningblocksExtendingabasetemplateComments
VisibleInvisible
IntroductiontoPugWhatisPug?It'satemplateengineforserver-sideNode.jsapplications.
Expressiscapableofhandlingserver-sidetemplateengines.Templateenginesallowustoadddatatoaview,andgenerateHTMLdynamically.
Pugisanewnameforanoldthing.It'sJade2.0.
ThePugGuide
27
ThenamewaschangedfromJadetoPugduetoatrademarkissuein2016,whentheprojectreleasedversion2.YoucanstilluseJade,akaPug1.0,butgoingforward,it'sbesttousePug2.0
AlsoseethedifferencesbetweenJadeandPug
ExpressusesJadeasthedefault.JadeistheoldversionofPug,specificallyPug1.0.
AlthoughthelastversionofJadeis3yearsold(atthetimeofwriting,summer2018),it'sstillthedefaultinExpressforbackwardcompatibilityreasons.
Inanynewproject,youshouldusePugoranotherengineofyourchoice.TheofficialsiteofPugishttps://pugjs.org/.
HowdoesPuglooklike
pHellofromFlavio
Thistemplatewillcreatea ptagwiththecontent HellofromFlavio.
Asyoucansee,Pugisquitespecial.Ittakesthetagnameasthefirstthinginaline,andtherestisthecontentthatgoesinsideit.
IfyouareusedtotemplateenginesthatuseHTMLandinterpolatevariables,likeHandlebars(describednext),youmightrunintoissues,especiallywhenyouneedtoconvertexistingHTMLtoPug.ThisonlineconverterfromHTMLtoJade(whichisverysimilar,butalittledifferentthanPug)willbeagreathelp:https://jsonformatter.org/html-to-jade
InstallPugInstallingPugisassimpleasrunning npminstall:
npminstallpug
SetupPugtobethetemplateengineinExpressandwheninitializingtheExpressapp,weneedtosetit:
constexpress=require('express')
constapp=express()
app.set('viewengine','pug')
ThePugGuide
28
app.set('views',path.join(__dirname,'views'))
YourfirstPugtemplateCreateanaboutview:
app.get('/about',(req,res)=>{
res.render('about')
})
andthetemplatein views/about.pug:
pHellofromFlavio
Thistemplatewillcreatea ptagwiththecontent HellofromFlavio.
InterpolatingvariablesinPugYoucaninterpolateavariableusing
app.get('/about',(req,res)=>{
res.render('about',{name:'Flavio'})
})
pHellofrom#{name}
InterpolateafunctionreturnvalueYoucaninterpolateafunctionreturnvalueusing
app.get('/about',(req,res)=>{
res.render('about',{getName:()=>'Flavio'})
})
pHellofrom#{getName()}
Addingidandclassattributestoelements
ThePugGuide
29
p#title
p.title
Setthedoctype
doctypehtml
Metatags
html
head
meta(charset='utf-8')
meta(http-equiv='X-UA-Compatible',content='IE=edge')
meta(name='description',content='Somedescription')
meta(name='viewport',content='width=device-width,initial-scale=1')
Addingscriptsandstyles
html
head
script(src="script.js")
script(src='//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js')
link(rel='stylesheet',href='css/main.css')
Inlinescripts
scriptalert('test')
script
(function(b,o,i,l,e,r){b.GoogleAnalyticsObject=l;b[l]||(b[l]=
function(){(b[l].q=b[l].q||[]).push(arguments)});b[l].l=+newDate;
e=o.createElement(i);r=o.getElementsByTagName(i)[0];
e.src='//www.google-analytics.com/analytics.js';
r.parentNode.insertBefore(e,r)}(window,document,'script','ga'));
ga('create','UA-XXXXX-X');ga('send','pageview');
Loops
ThePugGuide
30
ul
eachcolorin['Red','Yellow','Blue']
li=color
ul
eachcolor,indexin['Red','Yellow','Blue']
li='Colornumber'+index+':'+color
Conditionals
ifname
h2Hellofrom#{name}
else
h2Hello
else-ifworkstoo:
ifname
h2Hellofrom#{name}
elseifanotherName
h2Hellofrom#{anotherName}
else
h2Hello
SetvariablesYoucansetvariablesinPugtemplates:
-varname='Flavio'
-varage=35
-varroger={name:'Roger'}
-vardogs=['Roger','Syd']
IncrementingvariablesYoucanincrementanumericvariableusing ++:
age++
Assigningvariablestoelementvalues
ThePugGuide
31
p=name
span.age=age
IteratingovervariablesYoucanuse foror each.Thereisnodifference.
fordogindogs
li=dog
ul
eachdogindogs
li=dog
Youcanuse .lengthtogetthenumberofitems:
pThereare#{values.length}
whileisanotherkindofloop:
-varn=0;
ul
whilen<=5
li=n++
IncludingotherPugfilesInaPugfileyoucanincludeotherPugfiles:
includeotherfile.pug
DefiningblocksAwellorganizedtemplatesystemwilldefineabasetemplate,andthenalltheothertemplatesextendfromit.
ThePugGuide
32
Thewayapartofatemplatecanbeextendedisbyusingblocks:
html
head
script(src="script.js")
script(src='//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js')
link(rel='stylesheet',href='css/main.css')
blockhead
body
blockbody
h1Homepage
pwelcome
Inthiscaseoneblock, body,hassomecontent,while headdoesnot. headisintendedtobeusedtoaddadditionalcontenttotheheading,whilethe bodycontentismadetobeoverriddenbyotherpages.
ExtendingabasetemplateAtemplatecanextendabasetemplatebyusingthe extendskeyword:
extendshome.pug
Oncethisisdone,youneedtoredefineblocks.Allthecontentofthetemplatemustgointoblocks,otherwisetheenginedoesnotknowwheretoputthem.
Example:
extendshome.pug
blockbody
h1Anotherpage
pHey!
ul
liSomething
liSomethingelse
Youcanredefineoneormoreblocks.Theonesnotredefinedwillbekeptwiththeoriginaltemplatecontent.
CommentsCommentsinPugcanbeoftwotypes:visibleornotvisibleintheresultingHTML.
ThePugGuide
33
Visible
Inline:
//somecomment
Block:
//
some
comment
Invisible
Inline:
//-somecomment
Block:
//-
some
comment
ThePugGuide
34
MiddlewareAmiddlewareisafunctionthathooksintotheroutingprocess,andperformssomeoperationatsomepoint,dependingonwhatitwanttodo.
Amiddlewareisafunctionthathooksintotheroutingprocess,andperformssomeoperationatsomepoint,dependingonwhatitwanttodo.
It'scommonlyusedtoedittherequestorresponseobjects,orterminatetherequestbeforeitreachestheroutehandlercode.
It'saddedtotheexecutionstacklikethis:
app.use((req,res,next)=>{/**/})
Thisissimilartodefiningaroute,butinadditiontotheRequestandResponseobjectsinstances,wealsohaveareferencetothenextmiddlewarefunction,whichweassigntothevariable next.
Wealwayscall next()attheendofourmiddlewarefunction,topasstheexecutiontothenexthandler,unlesswewanttoprematurelyendtheresponse,andsenditbacktotheclient.
Youtypicallyusepre-mademiddleware,intheformof npmpackages.Abiglistoftheavailableonesishere.
Oneexampleis cookie-parser,whichisusedtoparsethecookiesintothe req.cookiesobject.Youinstallitusing npminstallcookie-parserandyoucanuseitlikethis:
constexpress=require('express')
constapp=express()
constcookieParser=require('cookie-parser')
app.get('/',(req,res)=>res.send('HelloWorld!'))
app.use(cookieParser())
app.listen(3000,()=>console.log('Serverready'))
Youcanalsosetamiddlewarefunctiontorunforspecificroutesonly,notforall,byusingitasthesecondparameteroftheroutedefinition:
constmyMiddleware=(req,res,next)=>{
/*...*/
next()
}
app.get('/',myMiddleware,(req,res)=>res.send('HelloWorld!'))
Middleware
35
Ifyouneedtostoredatathat'sgeneratedinamiddlewaretopassitdowntosubsequentmiddlewarefunctions,ortotherequesthandler,youcanusethe Request.localsobject.Itwillattachthatdatatothecurrentrequest:
req.locals.name='Flavio'
Middleware
36
ServingstaticfilesHowtoservestaticassetsdirectlyfromafolderinExpress
It'scommontohaveimages,CSSandmoreina publicsubfolder,andexposethemtotherootlevel:
constexpress=require('express')
constapp=express()
app.use(express.static('public'))
/*...*/
app.listen(3000,()=>console.log('Serverready'))
Ifyouhavean index.htmlfilein public/,thatwillbeservedifyounowhittherootdomainURL( http://localhost:3000)
Servingstaticfiles
37
SendfilesExpressprovidesahandymethodtotransferafileasattachment:`Response.download()`
Expressprovidesahandymethodtotransferafileasattachment: Response.download().
Onceauserhitsaroutethatsendsafileusingthismethod,browserswillprompttheuserfordownload.
The Response.download()methodallowsyoutosendafileattachedtotherequest,andthebrowserinsteadofshowingitinthepage,itwillsaveittodisk.
app.get('/',(req,res)=>res.download('./file.pdf'))
Inthecontextofanapp:
constexpress=require('express')
constapp=express()
app.get('/',(req,res)=>res.download('./file.pdf'))
app.listen(3000,()=>console.log('Serverready'))
Youcansetthefiletobesentwithacustomfilename:
res.download('./file.pdf','user-facing-filename.pdf')
Thismethodprovidesacallbackfunctionwhichyoucanusetoexecutecodeoncethefilehasbeensent:
res.download('./file.pdf','user-facing-filename.pdf',(err)=>{
if(err){
//handleerror
return
}else{
//dosomething
}
})
Sendfiles
38
SessionsHowtousesessionstoidentifyusersacrossrequests
BydefaultExpressrequestsaresequentialandnorequestcanbelinkedtoeachother.Thereisnowaytoknowifthisrequestcomesfromaclientthatalreadyperformedarequestpreviously.
Userscannotbeidentifiedunlessusingsomekindofmechanismthatmakesitpossible.
That'swhatsessionsare.
Whenimplemented,everyuserofyouAPIorwebsitewillbeassignedauniquesession,andthisallowsyoutostoretheuserstate.
We'llusethe express-sessionmodule,whichismaintainedbytheExpressteam.
Youcaninstallitusing
npminstallexpress-session
andonceyou'redone,youcaninstantiateitinyourapplicationwith
constsession=require('express-session')
Thisisamiddleware,soyouinstallitinExpressusing
constexpress=require('express')
constsession=require('express-session')
constapp=express()
app.use(session(
'secret':'343ji43j4n3jn4jk3n'
))
Afterthisisdone,alltherequeststotheapproutesarenowusingsessions.
secretistheonlyrequiredparameter,buttherearemanymoreyoucanuse.Itshouldbearandomlyuniquestringforyouapplication.
Thesessionisattachedtotherequest,soyoucanaccessitusing req.sessionhere:
app.get('/',(req,res,next)=>{
//req.session
}
Sessions
39
Thisobjectcanbeusedtogetdataoutofthesession,andalsotosetdata:
req.session.name='Flavio'
console.log(req.session.name)//'Flavio'
ThisdataisserializedasJSONwhenstored,soyouaresafetousenestedobjects.
Youcanusesessionstocommunicatedatatomiddlewarethat'sexecutedlater,ortoretrieveitlaterononsubsequentrequests.
Whereisthesessiondatastored?itdependsonhowyousetupthe express-sessionmodule.
Itcanstoresessiondatain
memory,notmeantforproductionadatabaselikeMySQLorMongoamemorycachelikeRedisorMemcached
Thereisabiglistof3rdpackagesthatimplementawidevarietyofdifferentcompatiblecachingstoresinhttps://github.com/expressjs/session
Allsolutionsstorethesessionidinacookie,andkeepthedataserver-side.Theclientwillreceivethesessionidinacookie,andwillsenditalongwitheveryHTTPrequest.
We'llreferencethatserver-sidetoassociatethesessionidwiththedatastoredlocally.
Memoryisthedefault,itrequiresnospecialsetuponyourpart,it'sthesimplestthingbutit'smeantonlyfordevelopmentpurposes.
ThebestchoiceisamemorycachelikeRedis,forwhichyouneedtosetupitsowninfrastructure.
AnotherpopularpackagetomanagesessionsinExpressis cookie-session,whichhasabigdifference:itstoresdataclient-sideinthecookie.Idonotrecommenddoingthatbecausestoringdataincookiesmeansthatit'sstoredclient-side,andsentbackandforthineverysinglerequestmadebytheuser.It'salsolimitedinsize,asitcanonlystore4kilobytesofdata.Cookiesalsoneedtobesecured,butbydefaulttheyarenot,sincesecureCookiesarepossibleonHTTPSsitesandyouneedtoconfigurethemifyouhaveproxies.
Sessions
40
ValidatinginputLearnhowtovalidateanydatacominginasinputinyourExpressendpoints
SayyouhaveaPOSTendpointthatacceptsthename,emailandageparameters:
constexpress=require('express')
constapp=express()
app.use(express.json())
app.post('/form',(req,res)=>{
constname=req.body.name
constemail=req.body.email
constage=req.body.age
})
Howdoyouserver-sidevalidatethoseresultstomakesure
nameisastringofatleast3characters?emailisarealemail?ageisanumber,between0and110?
ThebestwaytohandlevalidatinganykindofinputcomingfromoutsideinExpressisbyusingthe express-validatorpackage:
npminstallexpress-validator
Yourequirethe checkobjectfromthepackage:
const{check}=require('express-validator/check')
Wepassanarrayof check()callsasthesecondargumentofthe post()call.Everycheck()callacceptstheparameternameasargument:
app.post('/form',[
check('name').isLength({min:3}),
check('email').isEmail(),
check('age').isNumeric()
],(req,res)=>{
constname=req.body.name
constemail=req.body.email
constage=req.body.age
})
Validatinginput
41
NoticeIused
isLength()
isEmail()
isNumeric()
Therearemanymoreofthesemethods,allcomingfromvalidator.js,including:
contains(),checkifvaluecontainsthespecifiedvalueequals(),checkifvalueequalsthespecifiedvalueisAlpha()
isAlphanumeric()
isAscii()
isBase64()
isBoolean()
isCurrency()
isDecimal()
isEmpty()
isFQDN(),isafullyqualifieddomainname?isFloat()
isHash()
isHexColor()
isIP()
isIn(),checkifthevalueisinanarrayofallowedvaluesisInt()
isJSON()
isLatLong()
isLength()
isLowercase()
isMobilePhone()
isNumeric()
isPostalCode()
isURL()
isUppercase()
isWhitelisted(),checkstheinputagainstawhitelistofallowedcharacters
Youcanvalidatetheinputagainstaregularexpressionusing matches().
Datescanbecheckedusing
isAfter(),checkiftheentereddateisaftertheoneyoupassisBefore(),checkiftheentereddateisbeforetheoneyoupassisISO8601()
Validatinginput
42
isRFC3339()
Forexactdetailsonhowtousethosevalidators,refertohttps://github.com/chriso/validator.js#validators.
Allthosecheckscanbecombinedbypipingthem:
check('name')
.isAlpha()
.isLength({min:10})
Ifthereisanyerror,theserverautomaticallysendsaresponsetocommunicatetheerror.Forexampleiftheemailisnotvalid,thisiswhatwillbereturned:
{
"errors":[{
"location":"body",
"msg":"Invalidvalue",
"param":"email"
}]
}
Thisdefaulterrorcanbeoverriddenforeachcheckyouperform,using withMessage():
check('name')
.isAlpha()
.withMessage('Mustbeonlyalphabeticalchars')
.isLength({min:10})
.withMessage('Mustbeatleast10charslong')
Whatifyouwanttowriteyourownspecial,customvalidator?Youcanusethe customvalidator.
Inthecallbackfunctionyoucanrejectthevalidationeitherbythrowinganexception,orbyreturningarejectedpromise:
app.post('/form',[
check('name').isLength({min:3}),
check('email').custom(email=>{
if(alreadyHaveEmail(email)){
thrownewError('Emailalreadyregistered')
}
}),
check('age').isNumeric()
],(req,res)=>{
constname=req.body.name
constemail=req.body.email
constage=req.body.age
})
Validatinginput
43
Thecustomvalidator:
check('email').custom(email=>{
if(alreadyHaveEmail(email)){
thrownewError('Emailalreadyregistered')
}
})
canberewrittenas
check('email').custom(email=>{
if(alreadyHaveEmail(email)){
returnPromise.reject('Emailalreadyregistered')
}
})
Validatinginput
44
SanitizinginputYou'veseenhowtovalidateinputthatcomesfromtheoutsideworldtoyourExpressapp.
There'sonethingyouquicklylearnwhenyourunapublic-facingserver:nevertrusttheinput.
Evenifyousanitizeandmakesurethatpeoplecan'tenterweirdthingsusingclient-sidecode,you'llstillbesubjecttopeopleusingtools(evenjustthebrowserdevtools)toPOSTdirectlytoyourendpoints.
Orbotstryingeverypossiblecombinationofexploitknowntohumans.
Whatyouneedtodoissanitizingyourinput.
The express-validatorpackageyoualreadyusetovalidateinputcanalsoconvenientlyusedtoperformsanitization.
SayyouhaveaPOSTendpointthatacceptsthename,emailandageparameters:
constexpress=require('express')
constapp=express()
app.use(express.json())
app.post('/form',(req,res)=>{
constname=req.body.name
constemail=req.body.email
constage=req.body.age
})
Youmightvalidateitusing:
constexpress=require('express')
constapp=express()
app.use(express.json())
app.post('/form',[
check('name').isLength({min:3}),
check('email').isEmail(),
check('age').isNumeric()
],(req,res)=>{
constname=req.body.name
constemail=req.body.email
constage=req.body.age
})
Youcanaddsanitizationbypipingthesanitizationmethodsafterthevalidationones:
Sanitizinginput
45
app.post('/form',[
check('name').isLength({min:3}).trim().escape(),
check('email').isEmail().normalizeEmail(),
check('age').isNumeric().trim().escape()
],(req,res)=>{
//...
})
HereIusedthemethods:
trim()trimscharacters(whitespacebydefault)atthebeginningandattheendofastringescape()replaces <, >, &, ', "and /withtheircorrespondingHTMLentitiesnormalizeEmail()canonicalizesanemailaddress.Acceptsseveraloptionstolowercaseemailaddressesorsubaddresses(e.g. [email protected])
Othersanitizationmethods:
blacklist()removecharactersthatappearintheblacklistwhitelist()removecharactersthatdonotappearinthewhitelistunescape()replacesHTMLencodedentitieswith <, >, &, ', "and /ltrim()liketrim(),butonlytrimscharactersatthestartofthestringrtrim()liketrim(),butonlytrimscharactersattheendofthestringstripLow()removeASCIIcontrolcharacters,whicharenormallyinvisible
Forceconversiontoaformat:
toBoolean()converttheinputstringtoaboolean.Everythingexceptfor'0','false'and''returnstrue.Instrictmodeonly'1'and'true'returntruetoDate()converttheinputstringtoadate,ornulliftheinputisnotadatetoFloat()converttheinputstringtoafloat,orNaNiftheinputisnotafloattoInt()converttheinputstringtoaninteger,orNaNiftheinputisnotaninteger
Likewithcustomvalidators,youcancreateacustomsanitizer.
Inthecallbackfunctionyoujustreturnthesanitizedvalue:
constsanitizeValue=value=>{
//sanitize...
}
app.post('/form',[
check('value').customSanitizer(value=>{
returnsanitizeValue(value)
}),
],(req,res)=>{
constvalue=req.body.value
})
Sanitizinginput
46
Sanitizinginput
47
HandlingformsHowtoprocessformsusingExpress
ThisisanexampleofanHTMLform:
<formmethod="POST"action="/submit-form">
<inputtype="text"name="username"/>
<inputtype="submit"/>
</form>
Whentheuserpressthesubmitbutton,thebrowserwillautomaticallymakea POSTrequesttothe /submit-formURLonthesameoriginofthepage,sendingthedataitcontains,encodedas application/x-www-form-urlencoded.Inthiscase,theformdatacontainstheusernameinputfieldvalue.
Formscanalsosenddatausingthe GETmethod,butthevastmajorityoftheformsyou'llbuildwilluse POST.
TheformdatawillbesentinthePOSTrequestbody.
Toextractit,youwillusethe express.urlencoded()middleware,providedbyExpress:
constexpress=require('express')
constapp=express()
app.use(express.urlencoded())
Nowyouneedtocreatea POSTendpointonthe /submit-formroute,andanydatawillbeavailableon Request.body:
app.post('/submit-form',(req,res)=>{
constusername=req.body.username
//...
res.end()
})
Don'tforgettovalidatethedatabeforeusingit,using express-validator.
Handlingforms
48
FileuploadsinformsHowtomanagestoringandhandlingfilesuploadedviaforms,inExpress
ThisisanexampleofanHTMLformthatallowsausertouploadafile:
<formmethod="POST"action="/submit-form">
<inputtype="file"name="document"/>
<inputtype="submit"/>
</form>
Whentheuserpressthesubmitbutton,thebrowserwillautomaticallymakea POSTrequesttothe /submit-formURLonthesameoriginofthepage,sendingthedataitcontains,notencodedas application/x-www-form-urlencodedasanormalform,butas multipart/form-data.
Server-side,handlingmultipartdatacanbetrickyanderrorprone,sowearegoingtouseautilitylibrarycalledformidable.Here'stheGitHubrepo,ithasover4000starsandwellmaintained.
Youcaninstallitusing:
npminstallformidable
TheninyourNode.jsfile,includeit:
constexpress=require('express')
constapp=express()
constformidable=require('formidable')
Nowinthe POSTendpointonthe /submit-formroute,weinstantiateanewFormidableformusing formidable.IncomingFrom():
app.post('/submit-form',(req,res)=>{
newformidable.IncomingFrom()
})
Afterdoingso,weneedtoparsetheform.Wecandososynchronouslybyprovidingacallback,whichmeansallfilesareprocessed,andonceformidableisdone,itmakesthemavailable:
app.post('/submit-form',(req,res)=>{
newformidable.IncomingFrom().parse(req,(err,fields,files)=>{
if(err){
Fileuploadsinforms
49
console.error('Error',err)
throwerr
}
console.log('Fields',fields)
console.log('Files',files)
files.map(file=>{
console.log(file)
})
})
})
Oryoucanuseeventsinsteadofacallback,tobenotifiedwheneachfileisparsed,andotherevents,likeendingprocessing,receivinganon-filefield,oranerroroccurred:
app.post('/submit-form',(req,res)=>{
newformidable.IncomingFrom().parse(req)
.on('field',(name,field)=>{
console.log('Field',name,field)
})
.on('file',(name,file)=>{
console.log('Uploadedfile',name,file)
})
.on('aborted',()=>{
console.error('Requestabortedbytheuser')
})
.on('error',(err)=>{
console.error('Error',err)
throwerr
})
.on('end',()=>{
res.end()
})
})
Whateverwayyouchoose,you'llgetoneormoreFormidable.Fileobjects,whichgiveyouinformationaboutthefileuploaded.Thesearesomeofthemethodsyoucancall:
file.size,thefilesizeinbytesfile.path,thepaththisfileiswrittentofile.name,thenameofthefilefile.type,theMIMEtypeofthefile
Thepathdefaultstothetemporaryfolderandcanbemodifiedifyoulistentothe fileBeginevent:
app.post('/submit-form',(req,res)=>{
newformidable.IncomingFrom().parse(req)
.on('fileBegin',(name,file)=>{
form.on('fileBegin',(name,file)=>{
file.path=__dirname+'/uploads/'+file.name
})
Fileuploadsinforms
50
})
.on('file',(name,file)=>{
console.log('Uploadedfile',name,file)
})
//...
})
Fileuploadsinforms
51
AnExpressHTTPSserverwithaself-signedcertificateHowtocreateaself-signedHTTPScertificateforNode.jstotestappslocally
TobeabletoserveasiteonHTTPSfromlocalhostyouneedtocreateaself-signedcertificate.
Aself-signedcertificatewillbeenoughtoestablishasecureHTTPSconnection,althoughbrowserswillcomplainthatthecertificateisself-signedandassuchit'snottrusted.It'sgreatfordevelopmentpurposes.
TocreatethecertificateyoumusthaveOpenSSLinstalledonyoursystem.
Youmighthaveitinstalledalready,justtestbytyping opensslinyourterminal.
Ifnot,onaMacyoucaninstallitusing brewinstallopensslifyouuseHomebrew.OtherwisesearchonGoogle"howtoinstallopensslon".
OnceOpenSSLisinstalled,runthiscommand:
opensslreq-nodes-new-x509-keyoutserver.key-outserver.cert
Itwillasyouafewquestions.Thefirstisthecountryname:
Generatinga1024bitRSAprivatekey
...........++++++
.........++++++
writingnewprivatekeyto'server.key'
-----
Youareabouttobeaskedtoenterinformationthatwillbeincorporatedintoyourcertifi
caterequest.
WhatyouareabouttoenteriswhatiscalledaDistinguishedNameoraDN.
Therearequiteafewfieldsbutyoucanleavesomeblank
Forsomefieldstherewillbeadefaultvalue,
Ifyouenter'.',thefieldwillbeleftblank.
-----
CountryName(2lettercode)[AU]:
Thenyourstateorprovince:
StateorProvinceName(fullname)[Some-State]:
yourcity:
AnExpressHTTPSserverwithaself-signedcertificate
52
LocalityName(eg,city)[]:
andyourorganizationname:
OrganizationName(eg,company)[InternetWidgitsPtyLtd]:
OrganizationalUnitName(eg,section)[]:
Youcanleavealloftheseempty.
Justremembertosetthisto localhost:
CommonName(e.g.serverFQDNorYOURname)[]:localhost
andtoaddyouremailaddress:
EmailAddress[]:
That'sit!Nowyouhave2filesinthefolderwhereyouranthiscommand:
server.certistheself-signedcertificatefileserver.keyistheprivatekeyofthecertificate
BothfileswillbeneededtoestablishtheHTTPSconnection,anddependingonhowyouaregoingtosetupyourserver,theprocesstousethemwillbedifferent.
Thosefilesneedtobeputinaplacereachablebytheapplication,thenyouneedtoconfiguretheservertousethem.
Thisisanexampleusingthe httpscoremoduleandExpress:
consthttps=require('https')
constapp=express()
app.get('/',(req,res)=>{
res.send('HelloHTTPS!')
})
https.createServer({},app).listen(3000,()=>{
console.log('Listening...')
})
withoutaddingthecertificate,ifIconnectto https://localhost:3000thisiswhatthebrowserwillshow:
AnExpressHTTPSserverwithaself-signedcertificate
53
Withthecertificateinplace:
constfs=require('fs')
//...
https.createServer({
key:fs.readFileSync('server.key'),
cert:fs.readFileSync('server.cert')
},app).listen(3000,()=>{
console.log('Listening...')
})
Chromewilltellusthecertificateisinvalid,sinceit'sself-signed,andwillaskustoconfirmtocontinue,buttheHTTPSconnectionwillwork:
AnExpressHTTPSserverwithaself-signedcertificate
54
AnExpressHTTPSserverwithaself-signedcertificate
55
SetupLet'sEncryptforExpressHowtosetupHTTPSusingthepopularfreesolutionLet'sEncrypt
IfyourunaNode.jsapplicationonyourownVPS,youneedtomanagegettinganSSLcertificate.
TodaythestandardfordoingthisistouseLet'sEncryptandCertbot,atoolfromEFF,akaElectronicFrontierFoundation,theleadingnonprofitorganizationfocusedonprivacy,freespeech,andingeneralcivillibertiesinthedigitalworld.
Thesearethestepswe'llfollow:
InstallCertbotGeneratetheSSLcertificateusingCertbotAllowExpresstoservestaticfilesConfirmthedomainObtainthecertificateSetuptherenewal
InstallCertbotThoseinstructionsassumeyouareusingUbuntu,DebianoranyotherLinuxdistributionthatuses apt-get:
sudoadd-aptrepositoryppa:certbot/certbot
sudoapt-getupdate
sudoapt-getinstallcertbot
YoucanalsoinstallCertbotonaMactotest:
brewinstallcertbot
butyouwillneedtolinkthattoarealdomainname,inorderforittobeuseful.
GeneratetheSSLcertificateusingCertbotNowthatCertbotisinstalled,youcaninvokeittogeneratethecertificate.Youmustrunthisasroot:
SetupLet'sEncryptforExpress
56
certbotcertonly--manual
orcallsudo
sudocertbotcertonly--manual
Theinstallerwillaskyouthedomainofyourwebsite.
Thisistheprocessindetail.
Itasksfortheemail
➜sudocertbotcertonly--manualPassword:XXXXXXXXXXXXXXXXXX
Savingdebuglogto/var/log/letsencrypt/letsencrypt.log
Pluginsselected:Authenticatormanual,InstallerNone
Enteremailaddress(usedforurgentrenewalandsecuritynotices)(Enter'c'to
cancel):[email protected]
ItaskstoaccepttheToS:
PleasereadtheTermsofServiceat
https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf.Youmust
agreeinordertoregisterwiththeACMEserverat
https://acme-v02.api.letsencrypt.org/directory
(A)gree/(C)ancel:A
Itaskstosharetheemailaddress
WouldyoubewillingtoshareyouremailaddresswiththeElectronicFrontier
Foundation,afoundingpartneroftheLet'sEncryptprojectandthenon-profit
organizationthatdevelopsCertbot?We'dliketosendyouemailaboutourwork
encryptingtheweb,EFFnews,campaigns,andwaystosupportdigitalfreedom.
----------------------------------------
(Y)es/(N)o:Y
AndfinallywecanenterthedomainwherewewanttousetheSSLcertificate:
Pleaseenterinyourdomainname(s)(commaand/orspaceseparated)(Enter'c'
tocancel):copesflavio.com
Itasksifit'soktologyourIP:
Obtaininganewcertificate
Performingthefollowingchallenges:
SetupLet'sEncryptforExpress
57
http-01challengeforcopesflavio.com
----------------------------------------
NOTE:TheIPofthismachinewillbepubliclyloggedashavingrequestedthis
certificate.Ifyou'rerunningcertbotinmanualmodeonamachinethatisnot
yourserver,pleaseensureyou'reokaywiththat.
AreyouOKwithyourIPbeinglogged?
----------------------------------------
(Y)es/(N)o:y
Andfinallywegettotheverificationphase!
----------------------------------------
Createafilecontainingjustthisdata:
TS_oZ2-ji23jrio3j2irj3iroj_U51u1o0x7rrDY2E.1DzOo_voCOsrpddP_2kpoek2opeko2pke-UAPb21sW1c
AndmakeitavailableonyourwebserveratthisURL:
http://copesflavio.com/.well-known/acme-challenge/TS_oZ2-ji23jrio3j2irj3iroj_U51u1o0x7rrDY
2E
Nowlet'sleaveCertbotaloneforacoupleminutes.
Weneedtoverifyweownthedomain,bycreatingafilenamed TS_oZ2-ji23jrio3j2irj3iroj_U51u1o0x7rrDY2Einthe .well-known/acme-challenge/folder.Payattention!TheweirdstringIjustpastedchangeeverysingletime.
You'llneedtocreatethefolderandthefile,sincetheydonotexistbydefault.
InthisfileyouneedtoputthecontentthatCertbotprinted:
TS_oZ2-ji23jrio3j2irj3iroj_U51u1o0x7rrDY2E.1DzOo_voCOsrpddP_2kpoek2opeko2pke-UAPb21sW1c
Asforthefilename,thisstringisuniqueeachtimeyourunCertbot.
AllowExpresstoservestaticfilesInordertoservethatfilefromExpress,youneedtoenableservingstaticfiles.Youcancreatea staticfolder,andaddtherethe .well-knownsubfolder,thenconfigureExpresslikethis:
constexpress=require('express')
constapp=express()
//...
SetupLet'sEncryptforExpress
58
app.use(express.static(__dirname+'/static',{dotfiles:'allow'}))
//...
The dotfilesoptionismandatoryotherwise .well-known,whichisadotfileasitstartswithadot,won'tbemadevisible.Thisisasecuritymeasure,becausedotfilescancontainsensitiveinformationandtheyarebetteroffpreservedbydefault.
ConfirmthedomainNowruntheapplicationandmakesurethefileisreachablefromthepublicinternet,andgobacktoCertbot,whichisstillrunning,andpressENTERtogoonwiththescript.
ObtainthecertificateThat'sit!Ifallwentwell,Certbotcreatedthecertificate,andtheprivatekey,andmadethemavailableinafolderonyourcomputer(anditwilltellyouwhichfolder,ofcourse).
Nowcopy/pastethepathsintoyourapplication,tostartusingthemtoserveyourrequests:
constfs=require('fs')
consthttps=require('https')
constapp=express()
app.get('/',(req,res)=>{
res.send('HelloHTTPS!')
})
https.createServer({
key:fs.readFileSync('/etc/letsencrypt/path/to/key.pem'),
cert:fs.readFileSync('/etc/letsencrypt/path/to/cert.pem'),
ca:fs.readFileSync('/etc/letsencrypt/path/to/chain.pem')
},app).listen(443,()=>{
console.log('Listening...')
})
NotethatImadethisserverlistenonport443,soyouneedtorunitwithrootpermissions.
Also,theserverisexclusivelyrunninginHTTPS,becauseIused https.createServer().YoucanalsorunanHTTPserveralongsidethis,byrunning:
http.createServer(app).listen(80,()=>{
console.log('Listening...')
})
https.createServer({
SetupLet'sEncryptforExpress
59
key:fs.readFileSync('/etc/letsencrypt/path/to/key.pem'),
cert:fs.readFileSync('/etc/letsencrypt/path/to/cert.pem'),
ca:fs.readFileSync('/etc/letsencrypt/path/to/chain.pem')
},app).listen(443,()=>{
console.log('Listening...')
})
SetuptherenewalTheSSLcertificateisnotgoingtobevalidfor90days.Youneedtosetupanautomatedsystemforrenewingit.
How?Usingacronjob.
Acronjobisawaytoruntaskseveryintervaloftime.Itcanbeeeryweek,everyminute,everymonth.
Inourcasewe'llruntherenewalscripttwiceperday,asrecommendedintheCertbotdocumentation.
Firstfindouttheabsolutepathof certbotonyousystem.Iuse typecertbotonmacOStogetit,andinmycaseit's /usr/local/bin/certbot.
Here'sthescriptweneedtorun:
certbotrenew
Thisisthecronjobentry:
0*/12***root/usr/local/bin/certbotrenew>/dev/null2>&1
Itmeansrunitevery12hours,everyday:at00:00andat12:00.
Tip:Igeneratedthislineusinghttps://crontab-generator.org/
Addthisscripttoyourcrontab,byusingthecommand:
envEDITOR=picocrontab-e
Thisopensthe picoeditor(youcanchoosetheoneyouprefer).Youentertheline,save,andthecronjobisinstalled.
Oncethisisdone,youcanseethelistofcronjobsactiveusing
crontab-l
SetupLet'sEncryptforExpress
60
SetupLet'sEncryptforExpress
61