Node i «Express»-fart

Red: 10. juni 2026 | Pub: 23. mai 2026 | #node, #programmering

Express er et enkelt og effektivt rammeverk for Node som muliggjør kjappere og smidigere utvikling av web, server og API-applikasjoner. Det hørtes forlokkende ut, derfor prøvde jeg meg på en konvertering av Nodes HTTP-modul, over til det som mer eller mindre blir ansett som standard.

I forrige Node-innlegg etablerte jeg HTTP-koder og feilhåndtering i koden. Det sørger for at klienten får en korrekt og forståelig beskjed dersom noe går galt med programmet, eller om dataene som sendes inn til serveren ikke er gyldige.

Men hva med utviklerne og moderatorene av applikasjonen og serveren? Det er jo veldig viktig at de også kan følge med på hva som skjer, for lettere å kunne fikse eventuelle feil og mangler.

Da jeg søkte på loggføring i Node, var det to alternativer som stakk seg frem som standarder:

  • Morgan for loggføring av HTTP-forespørsler.
  • Winston for alt annet (feil, handlinger, applikasjonslogikk).

Dette er tredjeparts-Nodepakker, altså kode andre har laget, og som fritt kan lastes ned fra NPM .

Morgan kalles gjerne «middleware» (eller mellomledd på norsk) fordi det er kode som kjøres mellom innkommende forespørsel og utgående svar. Det er utviklet spesifikt som en såkalt Express-middleware. Derfor var det logisk at jeg først konverterte gjeldende oppsett av to-do-applikasjonen fra å bruke Nodes innebygde HTTP-modul til å benytte Express, en annen populær tredjepartspakke, som anses som en standard når det kommer til å utvikle serverapplikasjoner i Node.

Da måtte jeg først legge til rette for import av tredjeparts-moduler.

NPM

NPM er Nodes pakke-håndterer, som gjør oss i stand til å laste ned tusenvis av tilleggsfunksjoner og rammeverk med mer, som andre har utviklet og delt med verden. En Node-applikasjon består som regel av en rekke slike såkalte NPM-moduler.

For å bruke NPM i et prosjekt, må vi starte med kommandoen npm init. Det oppretter en package.json-fil på rot i prosjektet. Denne inneholder viktig informasjon om applikasjonen, som hvilke eksterne verktøy og NPM-pakker programmet er avhengig av for å fungere.

Når man kjører kommandoen får man først en rekke spørsmål:

  • package name (navnet på prosjektet ditt)
  • version (versjonsnummer, starter ofte på 1.0.0)
  • description (en kort beskrivelse av hva koden gjør)
  • entry point (hovedfilen som skal startes, ofte index.js)
  • git repository (hvis du har en tilhørende GitHub-lenke)
  • author (navnet ditt)
  • license (hvilken lisens koden skal ha, f.eks. MIT)

Man kan bare trykke enter på alle, eller npm init -y for automatisk «yes». Da opprettes følgende i package.json:

{
  "name": "todo-app",
  "version": "1.0.0",
  "description": "A simple TO-DO app.",
  "homepage": "https://github.com/MichaelHelgesen/todo-cli#readme",
  "bugs": {
    "url": "https://github.com/MichaelHelgesen/todo-cli/issues"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/MichaelHelgesen/todo-cli.git"
  },
  "license": "ISC",
  "author": "Mikke",
  "type": "commonjs",
  "main": "todo.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  }
}

Så fort man installerer en pakke, dukker det opp en mappe ved navn node_modules på rot. Det er her alle NPM-modulene lagres når vi installerer dem.

Og mange av disse modulene man laster ned, er igjen avhengig av en rekke andre moduler. Så selv om man laster ned én modul fra NPM, kan npm_modules plutselig inneholde en rekke andre, ukjente mapper.

Om en pakke er avhengig av andre NPM-moduler kan man sjekke ved å skrive følgende kommando:

npm view express dependencies

Da får man tilbake et objekt med komplett oversikt:

{
  qs: '^6.14.0',
  depd: '^2.0.0',
  etag: '^1.8.1',
  once: '^1.4.0',
  send: '^1.1.0',
  vary: '^1.1.2',
  debug: '^4.4.0',
  fresh: '^2.0.0',
  cookie: '^0.7.1',
  router: '^2.2.0',
  accepts: '^2.0.0',
  'type-is': '^2.0.1',
  parseurl: '^1.3.3',
  statuses: '^2.0.1',
  encodeurl: '^2.0.0',
  'mime-types': '^3.0.0',
  'proxy-addr': '^2.0.7',
  'body-parser': '^2.2.1',
  'escape-html': '^1.0.3',
  'http-errors': '^2.0.0',
  'on-finished': '^2.4.1',
  'content-type': '^1.0.5',
  finalhandler: '^2.1.0',
  'range-parser': '^1.2.1',
  'serve-static': '^2.2.0',
  'cookie-signature': '^1.2.1',
  'merge-descriptors': '^2.0.0',
  'content-disposition': '^1.0.0'
}

.gitignore

node_modules kan fort vokse seg stor og inneholde tusenvis av filer. Derfor er det best å legge den til i .gitignore, slik at den ikke lastes opp til Github, noe som kan ta svært lang tid. Men package.json og package-lock.json må følge programmet. De skal nemlig sikre at applikasjonen kjører med eksakt samme oppsett uansett hvor den lastes ned.

Førstnevnte inneholder som nevnt informasjon om programmet, men vil også utvides med en enkel liste over påkrevde moduler etter som de installeres:

{
  "name": "todo-app",
  "version": "1.0.0",
  "description": "A simple TO-DO app.",
  "homepage": "https://github.com/MichaelHelgesen/todo-cli#readme",
  "bugs": {
    "url": "https://github.com/MichaelHelgesen/todo-cli/issues"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/MichaelHelgesen/todo-cli.git"
  },
  "license": "ISC",
  "author": "",
  "type": "commonjs",
  "main": "todo.js",
  "scripts": {
    "test": "jest"
  },
  "dependencies": {
    "express": "^5.2.1",
    "morgan": "^1.10.1",
    "winston": "^3.19.0"
  },
  "devDependencies": {
    "jest": "^30.2.0"
  }
}

package-lock.json inneholder en langt mer omfattende liste av installerte moduler og deres avhengigheter. Den filen er derfor mye lenger, og inneholder versjonsnummer med mer. Det er med på å sikre at man laster ned eksakt samme versjoner som hovedapplikasjonen er laget med. Det skal sikre at ikke andre versjoner av moduler skal skape feil i applikasjonen.

Så når man laster ned en applikasjon med package.json og package-lock.json trenger man bare å skrive npm install for å hente ned alle de påkrevde modulene på maskinen.

Installere Express

Med NPM på plass installerte jeg Express med kommandoen npm install express.

For å teste at alt fungerte opprettet jeg en egen fil server.js, og satt opp et grunnleggende Express-skjelett:

const express = require('express')
const app = express()
const port = 3000

app.get('/', (req, res) => {
  res.send('Hello World!')
})

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})

Det fungerte:

Skjermbilde av nettleseren som er koblet til min første Express-server.

Her er forøvrig fremgangsmåten for å importere moduler som er lastet ned fra NPM:

const express = require('express')

Navnet som skal angis i require må hentes fra dokumentasjonen til modulen, med mindre man importerer egne moduler, noe som omtales senere i teksten. Denne metoden er den eldre såkalte CommonJS-metoden. Den nyere ES-metoden bruker «import/export»-syntaks. Siden Express-dokumentasjonen bruker CommonJS, velger jeg å gjøre det samme foreløpig.

Fra HTTP-modul til Express

Da lå alt til rette for å starte på selve konverteringen av HTTP-rutene til Express, men hvorfor gjorde jeg det?

Express er et såkalt rammeverk («framework») for HTTP-modulen. Det er på en måte noe som legges oppå HTTP-modulen, og som gir oss egne kommandoer, metoder og en egen syntaks som forenkler bruken av HTTP.

Express gjør slikt sett en del av arbeidet for oss, slik at vi slipper å skrive så mye kode. Og nettopp derfor har Express blitt en slags standard i bransjen.

GET

Jeg startet med GET-ruten. Utgangspunktet var følgende:

const server = http.createServer((req, res) => {
  if (req.method === "GET" && req.url === "/tasks") {
    const taskObjects = validateFile();
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({
      data: taskObjects,
    }));
  }
  //...

Med Express blir overnevnte HTTP-funksjonalitet redusert til:

app.get('/tasks', (req, res) => {
  const taskObjects = validateFile();
  return res.json(taskObjects);   
});

Det er en ganske stor reduksjon. Express komprimerer HTTP-metoden, banen og funksjonen («handler»):

app.METHOD(PATH, HANDLER);

Express fikser for eksempel writeHead og res.end automatisk. Og konverteringen til ønsket datatype, som JSON, er betydelig forenklet.

Dele opp applikasjonen

For å holde server.js oversiktlig er konvensjonen er å separere server-oppsettet og funksjonene. Derfor opprettet jeg controller.js for funksjonene, og importere dem iserver.js. Jeg opprettet filen i en egen mappe controllers/controller.js.

Og det er her jeg selv benyttet modulsystemet CommonJS, for å eksportere funksjonene fra controller.js:

module.exports = {
  validateFile,
  taskObjectGenerator,
  checkTask,
  deleteTask
}

Det er mest hensiktsmessig å kun eksportere de funksjonene som skal benyttes andre steder. De øvrige som benyttes internt i controller.js trenger ikke å tilgjengeliggjøres for “omverdenen”. Dette er en av fordelene med modulsystemet. Kode kan brytes opp i mindre biter som enkelt kan deles med andre. Det gjør at kode kan “gjemmes”. Slik sett unngår man for eksempel navnekonflikter som kan føre til feil.

Jeg kopierte den gamle filen, fjernet alt som hadde med HTTP-modulen og ruter å gjøre, og limet det inn i controller.js:

"use strict"

const fs = require("fs");
const tasksFileName = "tasks.json";

// Validate file
function validateFile() {
  // create file if it doesnt exist
  if (!fs.existsSync(`./${tasksFileName}`)) {
    fs.writeFileSync(tasksFileName, "[]")
  };
  return JSON.parse(fs.readFileSync(tasksFileName, "utf8"));
}

// create a backup
function backup() {
  fs.copyFileSync(tasksFileName, `${tasksFileName}.bak`);
}

// write files
function writeFile(taskObjects) {
  backup();
  fs.writeFileSync(tasksFileName, JSON.stringify(taskObjects, null, 2));
}

// check or uncheck task
function checkTask(id, status, taskObjects) {
  const checkForID = taskObjects.filter(obj => obj.id == id);
  if (checkForID.length) {
    taskObjects = taskObjects.map(task => {
      if(task.id == id) {
        return {...task, "done": status};
      } else {
        return task;    
      }
    });
    writeFile(taskObjects);
    return true;
  };
  return false;
}

// Create todo-object
function taskObjectGenerator(data, taskObjects) {
  if (data && data.trim()) {
    const object = {
      "createdDate": new Date().toISOString().split("T")[0],
      "done": false,
      "id": Date.now(),
      "task": data,
    }
    taskObjects.push(object);
    writeFile(taskObjects);
    return true;
  }
  return false;
};

// delete a task
function deleteTask(id, taskObjects) {
  const checkForID = taskObjects.filter(obj => obj.id === id);
  if (checkForID.length) {
    taskObjects = taskObjects.filter((obj) => obj.id != id);
    writeFile(taskObjects);
    return true;
  }
  return false;
}

module.exports = {
  validateFile,
  checkTask,
  taskObjectGenerator,
  deleteTask
}

I bunnen eksporterte jeg som nevnt funksjoner med metoden module.exports, som skaper et objekt med funksjoner som kan importeres i server.js på denne måten:

const {
  validateFile,
} = require("./controllers/controller.js");

MERK: I arbeidet med å dele inn i moduler endret jeg i controller.js henvisningen til filen med oppgaver tasks.json, som lå på rot, siden jeg flyttet funksjonen inn i en fil i mappen controllers. Det ble feil.

Modulen importeres jo i server.js, som fortsatt ligger på rot, og som er utgangspunktet for kjøringen av applikasjonen. Derfor trenger ikke banen til tasks.json endres. Utgangspunktet blir der hvor server.js kjøres fra.

Jeg fortsatte med POST-ruten.

POST

Utgangspunktet var følgende:

} else if (req.method === "POST" && req.url === "/tasks") {
    let body = "";
    req.on("data", (chunk) => {
      body += chunk.toString();
    });
    req.on("end", () => {
      try {
        let resObj = {};
        let statusCode;
        const data = JSON.parse(body);
        const taskObjects = validateFile();
        if (typeof data.task === "string" && taskObjectGenerator(data.task, taskObjects)) {
          resObj.data = `Success. To-do ${data.task} created`;
          statusCode = 200;
        } else {
          resObj.data = `Error. Creation of to-do ${data.task} failed. Name, date or ID is missing.`
          statusCode = 400;
        }
        res.writeHead(statusCode, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify(resObj));
      } catch (error) {
        res.writeHead(error.name === "SyntaxError" ? 400 : 500, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({
          "error": "Something went wrong."
        }));
      }
    });
  } 

Med Express reduseres det til:

app.post('/tasks', (req, res) => {
  let body = req.body;
  try {
    let resObj = {};
    let statusCode;
    const taskObjects = validateFile();
    if (typeof body.task === "string" && taskObjectGenerator(body.task, taskObjects)) {
      resObj.data = `Success. To-do ${body.task} created`;
      statusCode = 200;
    } else {
      resObj.data = `Error. Creation of to-do ${body.task} failed. Name, date or ID is missing.`
      statusCode = 400;
    } 
    res
    .status(statusCode)
    .json(resObj);
  } catch (error) {
    res
    .status(error.name === "SyntaxError" ? 400 : 500)
    .send("Something went wrong.");      
  }
}

Express tar seg av «chunking» og JSON-konverteringen, men en ting som er viktig å legge til, er Express sin middleware for dette. Den må defineres over rutene:

app.use(express.json());

Dette er en «middleware» som håndterer innkommende data, altså parsing av req.body.

MERK: res.json() kan sendes uten middleware. express.json() er kun for å håndtere innkommende data.

Denne funksjonen…

let body = "";
req.on("data", (chunk) => {
    body += chunk.toString();
});

… reduseres til følgende, med Express:

let body = req.body;

Express sjekker også automatisk når all innkommende data er levert, så req.on("end", () => { kan fjernes, og heller gå rett på try/catch.

app.post('/tasks', (req, res) => {
  let body = req.body;
  try {
    let resObj = {};
    let statusCode;
    const taskObjects = validateFile();

const data = JSON.parse(body); kan også slettes, siden Express allerede, ved hjelp av «middleware», har definert innkommende data som JSON.

if/else-blokken er ganske lik bortsett fra at data.task er byttet ut med body.task, og at res.writeHead og res.end er erstattet med en kjedet Express-variant:

if (typeof body.task === "string" && taskObjectGenerator(body.task, taskObjects)) {
    resObj.data = `Success. To-do ${body.task} created`;
    statusCode = 200;
} else {
    resObj.data = `Error. Creation of to-do ${body.task} failed. Name, date or ID is missing.`
    statusCode = 400;
} 
res.status(statusCode).json(resObj);
  }

MERK: .status må angis før responsobjektet sendes, som over, via .json(resObj).

PUT

Når det gjaldt statusendringer, var utgangspunktet følgende:

//...
 else if (req.method === "PUT" && req.url.startsWith("/tasks/") && req.url.split("/").length === 3) {
    let body = "";
    req.on("data", (chunk) => {
      body += chunk.toString();
    });
    req.on("end", () => {
      try {
        const data = JSON.parse(body);
        const taskObjects = validateFile();
        const taskID = Number(req.url.split("/")[2]);
        let resObj = {};
        let statusCode;
        if (typeof data.status === "boolean" && checkTask(Number(taskID), data.status , taskObjects)) {
          resObj.data = `Success. To-do with ID ${taskID} changed to ${data.status}`;
          statusCode = 200;
        } else {
          resObj.data = `Error. No to-do with ID ${taskID} found.`;
          statusCode = 404;
        }
        res.writeHead(statusCode, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify(resObj))
      } catch (error) {
        res.writeHead(error.name === "SyntaxError" ? 400 : 500, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({
          "error": `Something went wrong.`
        }));
      }
    });
  }

Med Express ble det redusert til følgende:

app.put('/tasks/:id', (req, res) => {
  let body = req.body;
  try {
    const taskObjects = validateFile();
    const taskID = Number(req.params.id);
    let resObj = {};
    let statusCode;
    if (typeof body.status === "boolean" && checkTask(taskID, body.status, taskObjects)) {
      resObj.data = `Success. To-do with ID ${taskID} changed to ${body.status}`;
      statusCode = 200;
    } else {
      resObj.data = `Error. No to-do with ID ${taskID} found.`;
      statusCode = 404;
    }
    res.status(statusCode).json(resObj);
  } catch (error) {
    console.log(error);
    res.status(error.name === "SyntaxError" ? 400 : 500).send(`Something went wrong.`);
  }
});

if-sjekken er en av elementene som virkelig forkortes med Express.

//...
else if (req.method === "PUT" && req.url.startsWith("/tasks/") && req.url.split("/").length === 3)
//...

url.startsWith og url.split, som sikrer ID-en i en URL slik at man ikke kan sende inn /tasks/123/abc, erstattes enkelt og greit av tasks/:id. Dette betyr at kun det som ligger umiddelbart etter /tasks/ er gyldig. Selve ID-en hentes med req.params.id.

Det betyr at const taskID = Number(req.url.split("/")[2]); kan erstattes med den enklere const taskID = Number(req.params.id);.

Resten av koden i PUT ble mer eller mindre det samme som POST.

DELETE

Sist, men ikke minst, kom jeg til sletting av en oppgave. Utgangspunktet var følgende:

 else if (req.method === "DELETE" && req.url.startsWith("/tasks/") && req.url.split("/").length === 3) {
    try {
      const taskObjects = validateFile();
      const taskID = Number(req.url.split("/")[2]);
      let resObj = {};
      let statusCode;
      if (deleteTask(Number(taskID), taskObjects)) {
        resObj.data = `Success. To-do with ID ${taskID} deleted`;
        statusCode = 200;
      } else {
        resObj.data = `Error. No to-do with ID ${taskID} found`;
        statusCode = 404;
      }
      res.writeHead(statusCode, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify(resObj))
    } catch (error) {
      res.writeHead(500, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({
        "error": "Something went wrong."
      }));
    }

Express gjør det på følgende måte:

app.delete('/tasks/:id', (req, res) => {
  try {
    const taskObjects = validateFile();
    const taskID = Number(req.params.id);
    let resObj = {};
    let statusCode;
    if (deleteTask(taskID, taskObjects)) {
        resObj.data = `Success. To-do with ID ${taskID} deleted`;
        statusCode = 200;
      } else {
        resObj.data = `Error. No to-do with ID ${taskID} found`;
        statusCode = 404;
      }
    res.status(statusCode).json(resObj);
    } catch (error) {
      console.log(error);
      res.status(500).send("Something went wrong");
    }
});

Express Routes

En siste ting jeg gjorde, var å flytte endepunktene til en egen fil routes/tasks.js. Dette er konvensjonen i Express-applikasjoner, å benytte Express Router.

Det vil si at alle endepunkter som hører sammen, slik som alle disse gjør siden de alle bruker banen tasks, flyttes til en egen fil, og eksporteres som et router-objekt.

Det gjør at serverfilen holdes ren og oversiktlig, og er ment som kun en start av serveren. Router fungerer som en middleware, og må importeres og defineres i server.js som egen app.use:

const express = require('express');
const taskRoutes = require('./routes/tasks');

const app = express();
const port = 3000;

// Express middleware
app.use(express.json());
app.use('/tasks', taskRoutes);

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})

Det denne gjør, er å sende alle henvendelser fra /tasks videre til /routes/tasks.js for videre behandling.

I /routes/tasks.js opprettet jeg en instans av klassen Router, som har en rekke metoder som .get, .post, .delete og så videre.

Disse metodene defineres med argumenter og funksjoner applikasjonen trenger, før hele Router-objektet til slutt eksporteres:

const express = require('express');
const router = express.Router();
const {
  validateFile,
  taskObjectGenerator,
  checkTask,
  deleteTask
} = require("../controllers/controller.js");

router.get('/', (req, res) => {
  const taskObjects = validateFile();
  return res.json(taskObjects);   
});

router.post('/', (req, res) => {
  let body = req.body;
  try {
    let resObj = {};
    let statusCode;
    const taskObjects = validateFile();
    if (typeof body.task === "string" && taskObjectGenerator(body.task, taskObjects)) {
      resObj.data = `Success. To-do ${body.task} created`;
      statusCode = 200;
    } else {
      resObj.data = `Error. Creation of to-do ${body.task} failed. Name, date or ID is missing.`
      statusCode = 400;
    } 
    res
    .status(statusCode)
    .json(resObj);
  } catch (error) {
    res
    .status(error.name === "SyntaxError" ? 400 : 500)
    .send("Something went wrong.");      
  }
});

router.put('/:id', (req, res) => {
  let body = req.body;
  try {
    const taskObjects = validateFile();
    const taskID = Number(req.params.id);
    let resObj = {};
    let statusCode;
    if (typeof body.status === "boolean" && checkTask(taskID, body.status, taskObjects)) {
      resObj.data = `Success. To-do with ID ${taskID} changed to ${body.status}`;
      statusCode = 200;
    } else {
      resObj.data = `Error. No to-do with ID ${taskID} found.`;
      statusCode = 404;
    }
    res.status(statusCode).json(resObj);
  } catch (error) {
    console.log(error);
    res.status(error.name === "SyntaxError" ? 400 : 500).send(`Something went wrong.`);
  }
});

router.delete('/:id', (req, res) => {
  try {
    const taskObjects = validateFile();
    const taskID = Number(req.params.id);
    let resObj = {};
    let statusCode;
    if (deleteTask(taskID, taskObjects)) {
        resObj.data = `Success. To-do with ID ${taskID} deleted`;
        statusCode = 200;
      } else {
        resObj.data = `Error. No to-do with ID ${taskID} found`;
        statusCode = 404;
      }
    res.status(statusCode).json(resObj);
    } catch (error) {
      console.log(error);
      res.status(500).send("Something went wrong");
    }
});

module.exports = router

404

Express lager selv en 404 hvis den kommer til et endepunkt som mangler, men den genereres som HTML. Jeg ville prøve å lage en JSON-versjon.

Den etableres under /tasks-rutene, siden det er siste stoppested.

server.js ser nå slik ut:

const express = require('express');
const taskRoutes = require('./routes/tasks');

const app = express();
const port = 3000;

// Express middleware
app.use(express.json());
app.use('/tasks', taskRoutes);
app.use((req, res) => {res.status(404).json('Not found')});

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`);
})

Dette var en lærerik øvelse, og det er helt klart at Express forenkler og effektiviserer etablering, oppsett og vedlikehold av HTTP-metoder.

I tillegg til Express i seg selv, lærte jeg hvordan en Express-applikasjon organiseres, med funksjoner i ./controllers/controller.js, endepunkt-håndtering i ./routes/tasks.js og en forenklet server.js som importerer disse utvidede filene.

Neste på programmet er å logge hendelser og feil til filer, som gjør det mulig å monitorere aktivitet, samt å få skikkelige feilmeldinger når noe går galt.


Ris, ros eller respons?

Send meg gjerne om du har en kommentar, korrektur eller konstruktiv kritikk til denne saken.