table of contents · express is a node.js web framework. node.js is an amazing tool for building...
TRANSCRIPT
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