Sovelluksen tietokantaan tallettamalle datan muodolle on usein tarve asettaa joitain ehtoja. Sovelluksemme ei esim. hyväksy muistiinpanoja, joiden sisältö eli content kenttä puuttuu. Muistiinpanon oikeellisuus tallennetaan sen luovassa metodissa:

app.post('/api/notes', (request, response) => {
  const body = request.body
  if (body.content === undefined) {    return response.status(400).json({ error: 'content missing' })  }
  // ...
})

Eli jos muistiinpanolla ei ole kenttää content, vastataan pyyntöön statuskoodilla 400 bad request.

Routejen tapahtumakäsittelijöissä tehtävää tarkastusta järkevämpi tapa tietokantaan talletettavan tiedon oikean muodon määrittelylle ja tarkastamiselle on Mongoosen validointitoiminnallisuuden käyttö.

Kullekin talletettavan datan kentälle voidaan määritellä validointisääntöjä skeemassa:

const noteSchema = new mongoose.Schema({
  content: {    type: String,    minlength: 5,    required: true  },  date: {     type: Date,    required: true  },  important: Boolean
})

Kentän content pituuden vaaditaan nyt olevan vähintään 5 merkkiä. Kentälle date taas on asetettu ehdoksi että sillä on oltava joku arvo, eli kenttä ei voi olla tyhjä. Sama ehto on asetettu myös kentälle content, sillä minimipituuden tarkistava ehto ei huomioi tilannetta, missä kentällä ei ole mitään arvoa. Kentälle important ei ole asetettu mitään ehtoa, joten se on määritelty edelleen yksinkertaisemmassa muodossa.

Esimerkissä käytetyt validaattorit minlength ja required ovat Mongooseen sisäänrakennettuja validointisääntöjä. Mongoosen custom validator -ominaisuus mahdollistaa mielivaltaisten validaattorien toteuttamisen, jos valmiiden joukosta ei löydy tarkoitukseen sopivaa.

Jos tietokantaan yritetään tallettaa validointisäännön rikkova olio, heittää tallennusoperaatio poikkeuksen. Muutetaan uuden muistiinpanon luomisesta huolehtivaa käsittelijää siten, että se välittää mahdollisen poikkeuksen virheenkäsittelijämiddlewaren huolehdittavaksi:

app.post('/api/notes', (request, response, next) => {
  const body = request.body

  const note = new Note({
    content: body.content,
    important: body.important || false,
    date: new Date(),
  })

  note.save()
    .then(savedNote => {
      response.json(savedNote.toJSON())
    })
    .catch(error => next(error))})

Laajennetaan virheenkäsittelijää huomioimaan validointivirheet:

const errorHandler = (error, request, response, next) => {
  console.error(error.message)

  if (error.name === 'CastError' && error.kind == 'ObjectId') {
    return response.status(400).send({ error: 'malformatted id' })
  } else if (error.name === 'ValidationError') {    return response.status(400).json({ error: error.message })  }

  next(error)
}

Validoinnin epäonnistuessa palautetaan validaattorin oletusarvoinen virheviesti:

fullstack content

Promisejen ketjutus

Useat routejen tapahtumankäsittelijöistä muuttivat palautettavan datan oikeaan formaattiin kutsumalla palautetuille olioille niiden metodia toJSON. Esimimerkiksi uuden muistiinpanon luomisessa metodia kutsutaan then:in parametrina palauttamalle oliolle:

app.post('/api/notes', (request, response, next) => {
  // ...

  note.save()
    .then(savedNote => {
      response.json(savedNote.toJSON())
    })
    .catch(error => next(error)) 
})

Voisimme tehdä saman myös hieman tyylikkäämmin promiseja ketjuttamalla:

app.post('/api/notes', (request, response) => {
  // ...

  note
    .save()
    .then(savedNote => {      return savedNote.toJSON()    })    .then(savedAndFormattedNote => {      response.json(savedAndFormattedNote)    })     .catch(error => next(error)) 
})

Eli ensimmäisen then:in takaisinkutsussa otamme Mongoosen palauttaman olion savedNote ja formatoimme sen. Operaation tulos palautetaan returnilla. Kuten osassa 2 todettiin, promisen then-metodi palauttaa myös promisen. Eli kun palautamme savedNote.toJSON():n takaisinkutsufunktiosta, syntyy promise, jonka arvona on formatoitu muistiinpano. Pääsemme käsiksi arvoon rekisteröimällä then-kutsulla uuden tapahtumankäsittelijän.

Selviämme vieläkin tiiviimmällä koodilla käyttämällä nuolifunktion lyhempää muotoa:

app.post('/api/notes', (request, response) => {
  // ...

  note
    .save()
    .then(savedNote => savedNote.toJSON())    .then(savedAndFormattedNote => {
      response.json(savedAndFormattedNote)
    }) 
    .catch(error => next(error)) 
})

Esimerkkimme tapauksessa promisejen ketjutuksesta ei ole suurta hyötyä. Tilanne alkaa muuttua, jos joudumme tekemään useita peräkkäisiä asynkronisia operaatiota. Emme kuitenkaan mene asiaan sen tarkemmin. Tutustumme seuraavassa osassa Javascriptin async/await-syntaksiin, jota käyttämällä peräkkäisten asynkronisten operaatioiden tekeminen helpottuu oleellisesti.

Tietokantaa käyttävän version vieminen tuotantoon

Sovelluksen pitäisi toimia tuotannossa, eli Herokussa lähes sellaisenaan. Frontendin muutosten takia on tehtävä siitä uusi tuotantoversio ja kopioitava se backendiin.

Huomaa, että vaikka määrittelimme sovelluskehitystä varten ympäristömuuttujille arvot tiedostossa .env, tietokantaurlin kertovan ympäristömuuttujan arvo asetetaan Herokuun komentorivillä komennolla heroku config:set

heroku config:set MONGODB_URI=mongodb+srv://fullstack:secret@cluster0-ostce.mongodb.net/note-app?retryWrites=true

Sovelluksen pitäisi nyt toimia. Aina kaikki ei kuitenkaan mene suunnitelmien mukaan. Jos ongelmia ilmenee, heroku logs auttaa. Oma sovellukseni ei toiminut muutoksen jälkeen. Loki kertoi seuraavaa

fullstack content

eli tietokannan osoite olikin jostain syystä määrittelemätön. Komento heroku config paljasti että olin vahingossa määritellyt ympäristömuuttujan MONGO_URL kun koodi oletti sen olevan nimeltään MONGODB_URI.

Sovelluksen tämän hetkinen koodi on kokonaisuudessaan Githubissa, branchissä part3-5.

Lint

Ennen osan lopetusta katsomme vielä nopeasti paitsioon jäänyttä tärkeää työkalua lintiä. Wikipedian sanoin:

Generically, lint or a linter is any tool that detects and flags errors in programming languages, including stylistic errors. The term lint-like behavior is sometimes applied to the process of flagging suspicious language usage. Lint-like tools generally perform static analysis of source code.

Staattisesti tyypitetyissä, käännettävissä kielissä esim. Javassa ohjelmointiympäristöt, kuten NetBeans osaavat huomautella monista koodiin liittyvistä asioista, sellaisistakin, jotka eivät ole välttämättä käännösvirheitä. Erilaisten staattisen analyysin lisätyökalujen, kuten checkstylen avulla voidaan vielä laajentaa Javassa huomautettavien asioiden määrää koskemaan koodin tyylillisiä seikkoja, esim. sisentämistä.

Javascript-maailmassa tämän hetken johtava työkalu staattiseen analyysiin, eli "linttaukseen" on ESlint.

Asennetaan ESlint backendiin kehitysaikaiseksi riippuvuudeksi komennolla

npm install eslint --save-dev

Tämän jälkeen voidaan muodostaa alustava ESlint-konfiguraatio komennolla

node_modules/.bin/eslint --init

Vastaillaan kysymyksiin:

fullstack content

Konfiguraatiot tallentuvat tiedostoon .eslintrc.js:

module.exports = {
    'env': {
        'commonjs': true,
        'es6': true,
        'node': true
    },
    'extends': 'eslint:recommended',
    'globals': {
        'Atomics': 'readonly',
        'SharedArrayBuffer': 'readonly'
    },
    'parserOptions': {
        'ecmaVersion': 2018
    },
    'rules': {
        'indent': [
            'error',
            4
        ],
        'linebreak-style': [
            'error',
            'unix'
        ],
        'quotes': [
            'error',
            'single'
        ],
        'semi': [
            'error',
            'never'
        ]
    }
}

Muutetaan heti konfiguraatioista sisennystä määrittelevä sääntö, siten että sisennystaso on 2 välilyöntiä

"indent": [
    "error",
    2
],

Esim tiedoston index.js tarkastus tapahtuu komennolla

node_modules/.bin/eslint index.js

Kannattaa ehkä tehdä linttaustakin varten npm-skripti:

{
  // ...
  "scripts": {
    "start": "node index.js",
    "watch": "nodemon index.js",
    "lint": "eslint ."
  },
  // ...
}

Nyt komennot npm run lint suorittaa tarkastukset koko projektille.

Myös hakemistossa build oleva frontendin tuotantoversio tulee näin tarkastettua. Sitä emme kuitenkaan halua, eli tehdään projektin juureen tiedosto .eslintignore ja sille seuraava sisältö

build

Näin koko hakemiston build sisältö jätetään huomioimatta linttauksessa.

Lintillä on jonkin verran huomautettavaa koodistamme:

fullstack content

Ei kuitenkaan korjata ongelmia vielä.

Parempi vaihtoehto kuin linttauksen suorittaminen komentoriviltä on konfiguroida editorille lint-plugin, joka suorittaa linttausta koko ajan. Näin pääset korjaamaan pienet virheet välittömästi. Tietoja esim. Visual Studion ESlint-pluginsta täällä.

VS Coden ESlint-plugin alleviivaa tyylisääntöjä rikkovat kohdat punaisella:

fullstack content

Näin ongelmat on helppo korjata koodiin heti.

ESlintille on määritelty suuri määrä sääntöjä, joita on helppo ottaa käyttöön muokkaamalla tiedostoa .eslintrc.js.

Otetaan käyttöön sääntö eqeqeq joka varoittaa, jos koodissa yhtäsuuruutta verrataan muuten kuin käyttämällä kolmea = -merkkiä. Sääntö lisätään konfiguraatiotiedostoon kentän rules alle.

{
  // ...
  "rules": {
    // ...
    "eqeqeq": "error"
  },
}

Tehdään samalla muutama muukin muutos tarkastettaviin sääntöihin.

Estetään rivien lopussa olevat turhat välilyönnit, vaaditaan että aaltosulkeiden edessä/jälkeen on aina välilyönti ja vaaditaan myös konsistenttia välilyöntien käyttöä nuolifunktioiden parametrien suhteen:

{
  // ...
  "rules": {
    // ...
    "eqeqeq": "error",
    "no-trailing-spaces": "error",
    "object-curly-spacing": [
        "error", "always"
    ],
    "arrow-spacing": [
        "error", { "before": true, "after": true }
    ]
  },
}

Oletusarvoinen konfiguraatiomme ottaa käyttöön joukon valmiiksi määriteltyjä sääntöjä eslint:recommended

"extends": "eslint:recommended",

Mukana on myös console.log-komennoista varoittava sääntö. Yksittäisen sääntö on helppo kytkeä pois päältä määrittelemällä sen "arvoksi" konfiguraatiossa 0. Tehdään toistaiseksi näin säännölle no-console.

{
  // ...
  "rules": {
    // ...
    "eqeqeq": "error",
    "no-trailing-spaces": "error",
    "object-curly-spacing": [
        "error", "always"
    ],
    "arrow-spacing": [
        "error", { "before": true, "after": true }
    ],
    "no-console": 0  },
}

HUOM kun teet muutoksia tiedostoon .eslintrc.js, kannattaa muutosten jälkeen suorittaa linttaus komentoriviltä ja varmistaa että konfiguraatio ei ole viallinen:

fullstack content

Jos konfiguraatiossa on jotain vikaa, voi editorin lint-plugin näyttää mitä sattuu.

Monissa yrityksissä on tapana määritellä yrityksen laajuiset koodausstandardit ja näiden käyttöä valvova ESlint-konfiguraatio. Pyörää ei kannata välttämättä keksiä uudelleen ja voi olla hyvä idea ottaa omaan projektiin käyttöön joku jossain muualla hyväksi havaittu konfiguraatio. Viime aikoina monissa projekteissa on omaksuttu AirBnB:n Javascript-tyyliohjeet ottamalla käyttöön firman määrittelemä ESLint-konfiguraatio.

Sovelluksen tämän hetkinen koodi on kokonaisuudessaan Githubissa, branchissa part3-6.