Fra kommandolinjen til server i Node

Red: 16. mai 2026 | Pub: 1. mai 2026 | #node, #programmering

For at en applikasjon skal kunne brukes i en nettleser, må den kunne lytte til og svare på forespørsler. I dette innlegget tar jeg steget bort fra kommandolinjen og utforsker hvordan Nodes innebygde HTTP-modul lar oss etablere og kjøre en server.

I forrige Node-innlegg konverterte jeg fra Markdown til JSON for en ryddigere og mer fleksibel datastruktur, men applikasjonen fungerte kun med argumenter fra kommandolinjen.

Siden jeg ønsker å utvide applikasjonen til å benytte en server for lagring av data, og en nettleser som grensesnitt, må jeg benytte HTTP-modulen som svarer på HTTP-forespørsler fremfor kommandolinje-argumenter.

Til forskjell fra process.argv, hvor argumentene angis i det man starter applikasjonen, må jeg med et HTTP-API aktivt lytte til innkommende henvendelser, og reagere på dem. Det markerer en vesensforskjell i programmets oppførsel. Med process.argv avsluttes programmet automatisk etter at alle funksjoner har kjørt, mens ved å opprette en server og lytte til innkommende forespørsler, kjører programmet kontinuerlig.

Det er gjerne tre essensielle ting å ta hensyn til når man skal behandle innkommende HTTP-forespørsler:

  1. Henvendelsens metode. Er det for eksempel en forespørsel om å:
    • hente noe (GET)
    • legge til noe (POST)
    • slette noe (DELETE)
    • oppdatere noe (PUT)
  2. Banen forespørselen kommer fra («path»).
  3. Data som sendes inn.

Jeg startet med å importere HTTP-modulen, som er en standard Node-modul:

"use strict"

const http = require('node:http');

// Opprett en lokal server som skal motta data
const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({
    data: 'Hello World!',
  }));
});

server.listen(8000);

Deretter opprettet jeg et server-objekt, som tar en anonym «callback»-funksjon som argument. Denne «callback»-en kjører hver gang det kommer en forespørsel.

Serveren startes via metoden server.listen, som åpner en port i maskinen, og applikasjonen lytter kontinuerlig etter innkommende henvendelser herfra.

Gangen i applikasjonen med denne nye serverfunksjonen er nå som følger:

  • Ved oppstart opprettes server-instansen med en «callback»-funksjon.
  • Deretter sendes en I/O-forespørsel ut av Node, til systemets demultiplekser, med beskjed om å overvåke angitt port.
  • Kommer det en forespørsel skal hendelsen («eventen») returneres til Node-applikasjonen, og hendelsen skal knyttes til createServer-«callback»-en.
  • Da vet event-loopen i Node, som deretter tar over, at innkommende henvendelse skal knyttes til createServer-«callback-en», og kjører denne så snart den kan.

Forespørsel og respons

const server lagrer altså en instans av en Server-klasse via http.createServer, som importeres fra HTTP-modulen, og arver en rekke metoder og verdier via prototype-kjeden. createServer tar en anonym «callback»-funksjon som argument, og returnerer et server-objekt.

«Callback»-en tar to viktige objekter som argument: req, som er innkommende data, og res, som er applikasjonens respons som skal returneres til klienten.

res-objektet har også egne metoder, og vi ser to av dem i koden over. writeHead konstruerer en såkalt «header» som setter statuskode og annen informasjon som klientens nettleser reagerer på, og res.end sender selve responsen og avslutter forbindelsen.

const server arver også metoden listen, som faktisk starter serveren, og gir beskjed til demultiplekseren om å starte overvåkingen av angitt port:

  • Demultiplekseren oppdager innkommende forespørsel, og melder fra til Nodes event-kø «event queue».
  • Event loop kaller «callback»-en med req (som er data levert av demultiplekseren) og res (responsen som Node selv lager)
  • Applikasjonen leser req og konstruerer et svar
  • res.end() skriver svaret tilbake til porten, som deretter sendes til klienten.

Baner

Etter å ha vurdert HTTP-metoder opp mot applikasjonen så langt, så jeg behovet for metoden «ADD» for å registrere en oppgave, «PUT» for å endre status på en oppgave, «DELETE» for å slette og «GET» for å se dem.

I REST-konvensjonen er det vanlig å la metoden og ikke banen avgjøre jobben som skal utføres. Så istedenfor /tasks/add og /tasks/delete som separate baner, benyttet jeg /tasks, og tok sikte på å skille oppgavene via metoden, og ytterligere data i banen. Det er nemlig kombinasjonen av metode og bane som gjør hver forespørsel unik.

Så strukturen ble:

  • GET /tasks — liste oppgaver
  • POST /tasks — legge til oppgaver
  • PUT /tasks — markere ferdig/uferdig
  • DELETE /tasks — slette

Men hvordan skille mellom oppgaver som skal slettes, og endres, når alle bruker samme bane? ID-en til de respektive «todo»-ene må legges til.

DELETE /tasks/1
PUT /tasks/2

Da var planen klar for å opprette «if-else»-logikken. Jeg startet enkelt:

"use strict"

const http = require('node:http');

// Opprett en lokal server som skal motta data
const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'application/json' });
  if (req.method === "GET" && req.url === "/tasks") {
    res.end(JSON.stringify({
      data: 'Hello World! This is a GET-request',
    }));
  } else if (req.method === "POST" && req.url === "/tasks") {
      res.end(JSON.stringify({
        data: 'Hello World! This is a POST-request',
      }));
  } else if (req.method === "PUT" && req.url === "/tasks/1") {
      res.end(JSON.stringify({
        data: 'Hello World! This is a PUT-request',
      }));
  } else if (req.method === "DELETE" && req.url === "/tasks/1") {
      res.end(JSON.stringify({
        data: 'Hello World! This is a DELETE-request',
      }));
  } else {
    res.end(JSON.stringify({
      data: 'Hello World!',
    }));
  }
});


server.listen(8000);

Etter å ha startet applikasjonen sjekket jeg alle metodene og endepunktene via curl:

curl -X GET http://localhost:8000/tasks
curl -X POST http://localhost:8000/tasks
curl -X DELETE http://localhost:8000/tasks/1
curl -X PUT http://localhost:8000/tasks/1

Baner med ID

Hardkodede ID-er som over kan vi selvsagt ikke bruke, så jeg endret fra å sjekke etter én spesifikk bane, til at banen måtte inneholde “/tasks/” og kun én streng til, slik at vi ikke får positive treff på f.eks. /tasks/123/abc/xyz. Jeg ønsket kun /tasks/123.

} else if (req.method === "PUT" && req.url.startsWith("/tasks/") && req.url.split("/").length === 3) {
      res.end(JSON.stringify({
        data: 'Hello World! This is a PUT-request',
      }));
  } else if (req.method === "DELETE" && req.url.startsWith("/tasks/") && req.url.split("/").length === 3) {
      res.end(JSON.stringify({
        data: 'Hello World! This is a DELETE-request',
      }));

Oppdatere funksjoner

Neste steg var å endre funksjonene til å reagere på metodene og banene som kommer inn. Jeg startet med listingen av oppgaver.

Vise registrerte oppgaver

Først trengte jeg å oppdatere validateFile, som sjekker om JSON-filen eksisterer og returnerer et JSON-objekt med oppgaver. Deretter måtte jeg returnere objektet med oppgaver i res-objektet, slik at klienten får se oppgavene.

if (req.method === "GET" && req.url === "/tasks") {
        const taskObjects = validateFile();
        res.end(JSON.stringify({
        data: taskObjects,
    }));
}

Registrere nye oppgaver

Når det kommer til å registrere nye oppgaver måtte jeg lese data fra innkommende forespørsel, for å kunne finne oppgaveteksten.

Dataene kommer inn som biter eller «chunks» i en kontinuerlig strøm til det er ferdig levert, og må samles i en variabel før de kan behandles. Dette er logikken:

req.on("data", (chunk) => {
    console.log(chunk);
});
req.on("end", () => {
    console.log("ferdig");
});

Jeg sendte inn et objekt via Curl, curl -X POST http://localhost:8000/tasks -H "Content-Type: application/json" -d '{"task": "test oppgave"}', og fikk følgende:

<Buffer 7b 22 74 61 73 6b 22 3a 20 22 74 65 73 74 20 6f 70 70 67 61 76 65 22 7d>
ferdig

Det var ikke så nyttig. Dataene måtte først typebestemmes, så jeg konverterte det til en streng:

req.on("data", (chunk) => {
          console.log(chunk.toString());
      });
      req.on("end", () => {
          console.log("ferdig");
      });

Da ble resultatet som følger:

{"task": "test oppgave"}
ferdig

data og end er forøvrig predefinerte «event»-navn som er en del av Nodes strøm-API. data fyres hver gang en ny «chunk» ankommer, og end kjører når strømmen er ferdig.

Dette ble utgangspunktet for å registrere nye oppgaver via POST-metoden:

else if (req.method === "POST" && req.url === "/tasks") {
      let body = "";
      req.on("data", (chunk) => {
          body += chunk.toString();
      });
      req.on("end", () => {
        const data = JSON.parse(body);
        const taskObjects = validateFile();
        taskObjectGenerator(data.task, taskObjects);
          res.end(JSON.stringify({
            data: 'Hello World! This is a POST-request',
          }));
      });
  }

res.end ble flyttet inn i req.on("end"..., fordi det er en sikkerhet om at all data er mottatt og kan derfor behandles videre, til vi skal sende tilbake en respons.

Turen gikk deretter videre til sletting av oppgaver.

Slette oppgaver

Jeg hentet ut ID-en fra banen, og sendte den inn til deleteTask-funksjonen. Jeg konverterte først til nummer, slik at deleteTask garanteres å få riktig type, og ikke må utføre typekonvertering selv.

else if (req.method === "DELETE" && req.url.startsWith("/tasks/") && req.url.split("/").length === 3) {
      const taskObjects = validateFile();
      deleteTask(Number(req.url.split("/")[2]), taskObjects);
      res.end(JSON.stringify({
        data: 'DELETING',
      }));
  }

Oppdatere status

Så fulgte endring av status. Da måtte jeg hente ID igjen, og samle datastrømmen i en variabel for å kunne lese hvilken status oppgaven skal endres til.

 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", () => {
        const data = JSON.parse(body);
        const taskObjects = validateFile();
        checkTask(Number(req.url.split("/")[2]), data.status , taskObjects)
          res.end(JSON.stringify({
            data: 'Status change registered',
          }));
      });

Dataene er da tenkt å sendes inn på følgende vis:

curl -X PUT http://localhost:8000/tasks/1778614473071 -H "Content-Type: application/json" -d '{"status": false}'

Merk at status angis som booleansk verdi. Det er noe JSON støtter.

Jeg sendte oppgavens ID, ny status og oppgavelisten til checkTask-funksjonen, og kan bruke status som den er, siden det er en bolsk verdi:

// 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);
  }
}

Nå har jeg en fungerende, enkel http-versjon av applikasjonen. Neste steg er statuskoder og feilhåndtering.

Under følger hele koden på nåværende tidspunkt:

"use strict"
const http = require('node:http');
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"));
}

// write files
function writeFile(taskObjects) {
  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);
  }
}

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

// 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);
    }
 }

// Opprett en lokal server som skal motta data
const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'application/json' });
  if (req.method === "GET" && req.url === "/tasks") {
    const taskObjects = validateFile();
    res.end(JSON.stringify({
      data: taskObjects,
    }));
  } else if (req.method === "POST" && req.url === "/tasks") {
      let body = "";
      req.on("data", (chunk) => {
          body += chunk.toString();
      });
      req.on("end", () => {
        const data = JSON.parse(body);
        const taskObjects = validateFile();
        taskObjectGenerator(data.task, taskObjects);
          res.end(JSON.stringify({
            data: 'TODO-registered',
          }));
      });
  } 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", () => {
        const data = JSON.parse(body);
        const taskObjects = validateFile();
        checkTask(Number(req.url.split("/")[2]), data.status , taskObjects)
          res.end(JSON.stringify({
            data: 'Status change registered',
          }));
      });
  } else if (req.method === "DELETE" && req.url.startsWith("/tasks/") && req.url.split("/").length === 3) {
      const taskObjects = validateFile();
      deleteTask(Number(req.url.split("/")[2]), taskObjects);
      res.end(JSON.stringify({
        data: 'DELETING',
      }));
  } else {
    res.end(JSON.stringify({
      data: 'Hello World!',
    }));
  }
});


server.listen(8000);

Ris, ros eller respons?

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