← Alle guider

Komplet guide: Backends og APIs med Node.js + Express

En praktisk, begyndervenlig guide til at bygge backends og REST APIs med Node.js og Express. Skrevet på dansk.


Indholdsfortegnelse

  1. Hvad er Node.js?
  2. Hvornår skal du bruge det?
  3. Kom i gang
  4. Projektstruktur
  5. Express fundamentals
  6. REST API design
  7. Middleware
  8. Database
  9. Validering
  10. Fejlhåndtering
  11. Autentificering
  12. Filoploading
  13. Real-time
  14. Deployment
  15. Pris
  16. Fordele og ulemper
  17. Eksempel-projekt: Todo API med auth

1. Hvad er Node.js?

Node.js er en JavaScript runtime bygget på Chrome's V8 JavaScript-motor. Det betyder, at du kan køre JavaScript på serveren — ikke kun i browseren.

Kernekoncepter

Hvem bruger Node.js?

Virksomhed Hvad de bruger det til
Netflix Server-side rendering, API gateway
Uber Microservices, real-time data
LinkedIn Mobil-API, real-time notifikationer
PayPal Betalings-API, microservices
NASA Kritiske dataopsamlingssystemer

Eksempel: Hello World i ren Node.js

const http = require('http');

    const server = http.createServer((req, res) => {
      res.writeHead(200, { 'Content-Type': 'text/plain' });
      res.end('Hej fra Node.js!');
    });

    server.listen(3000, () => {
      console.log('Serveren kører på http://localhost:3000');
    });
    

Dette virker, men det er omstændeligt at bygge større applikationer sådan. Derfor bruger vi Express — et minimalistisk framework der gør alt meget nemmere.


2. Hvornår skal du bruge Node.js?

Node.js er ikke det rigtige valg til alt. Her er hvor det skinner:

Perfekt til

Anvendelse Hvorfor
REST APIs Hurtig udvikling, stort økosystem, JSON er indbygget
Real-time apps (chat, live notifikationer) Event-driven arkitektur, WebSocket understøttelse
Microservices Letvægts, hurtig startup-tid, let at containerisere
Serverless functions Kold start på ~50ms, AWS Lambda understøttelse
Prototyper og MVP'er Hurtigste vej fra idé til kørende API

Måske ikke det bedste valg til

Anvendelse Hvorfor
CPU-tunge beregninger Single-threaded, blokerer event loop
Tunge maskinlæringsopgaver Brug Python eller C++ i stedet
Stærkt typede systemer Overvej Go, Java eller C#

Real-time eksempel: Chat-server med Socket.io

const express = require('express');
    const http = require('http');
    const { Server } = require('socket.io');

    const app = express();
    const server = http.createServer(app);
    const io = new Server(server);

    io.on('connection', (socket) => {
      console.log('Bruger forbundet:', socket.id);

      socket.on('besked', (data) => {
        io.emit('besked', data); // Send til alle forbundne
      });

      socket.on('disconnect', () => {
        console.log('Bruger afbrudt:', socket.id);
      });
    });

    server.listen(3000);
    

3. Kom i gang

Trin 1: Installer Node.js

Gå til nodejs.org og download den stable LTS-version. Det installerer både node og npm.

# Tjek din version
    node --version    # f.eks. v20.11.0
    npm --version     # f.eks. 10.2.4
    

Trin 2: Opret et nyt projekt

mkdir mit-projekt
    cd mit-projekt
    npm init -y
    

Dette opretter en package.json fil. Flaget -y accepterer alle standardindstillinger.

Trin 3: Installer Express

npm install express
    

Trin 4: Hello World

Opret en fil index.js:

const express = require('express');
    const app = express();
    const PORT = process.env.PORT || 3000;

    app.get('/', (req, res) => {
      res.json({ message: 'Hej fra Express!' });
    });

    app.listen(PORT, () => {
      console.log(`Serveren kører på http://localhost:${PORT}`);
    });
    

Kør den:

node index.js
    

Åbn http://localhost:3000 i din browser. Du burde se JSON-svaret.

Trin 5: Brug nodemon til udvikling

npm install -D nodemon
    

Tilføj en script til package.json:

{
      "scripts": {
        "start": "node index.js",
        "dev": "nodemon index.js"
      }
    }
    

Nu genstarter serveren automatisk når du gemmer filer:

npm run dev
    

4. Projektstruktur

For små projekter kan alt ligge i én fil. Men når dit projekt vokser, har du brug for struktur. Her er en klassisk opdeling:

mit-projekt/
    ├── src/
    │   ├── controllers/      # Forretningslogik
    │   │   ├── auth.controller.js
    │   │   ├── user.controller.js
    │   │   └── todo.controller.js
    │   ├── models/           # Datamodeller
    │   │   ├── User.js
    │   │   └── Todo.js
    │   ├── routes/           # API-ruter
    │   │   ├── auth.routes.js
    │   │   ├── user.routes.js
    │   │   └── todo.routes.js
    │   ├── middleware/        # Custom middleware
    │   │   ├── auth.js
    │   │   └── errorHandler.js
    │   ├── config/           # Konfiguration
    │   │   ├── database.js
    │   │   └── env.js
    │   ├── utils/            # Hjælpefunktioner
    │   │   └── helpers.js
    │   └── app.js            # Express app setup
    ├── tests/                # Tests
    ├── .env                  # Hemmelige nøgler (aldrig i git)
    ├── .gitignore
    ├── package.json
    └── index.js              # Entry point
    

Hvad hører hjemme hvor?

Mappe Ansvar
controllers/ Modtager request, kalder model/service, returnerer response
models/ Definerer datastruktur og database-interaktion
routes/ Matcher URL + HTTP-metode til den rigtige controller
middleware/ Kører kode før/efter controllers (auth, logging, validering)
config/ Database URLs, API-nøgler, port-numre
utils/ Rene funktioner der ikke hører hjemme andre steder

Eksportér og importér moduler

// routes/todo.routes.js
    const express = require('express');
    const router = express.Router();
    const todoController = require('../controllers/todo.controller');

    router.get('/', todoController.getAll);
    router.post('/', todoController.create);
    router.get('/:id', todoController.getById);

    module.exports = router;
    
// app.js
    const express = require('express');
    const todoRoutes = require('./routes/todo.routes');

    const app = express();
    app.use(express.json());
    app.use('/api/todos', todoRoutes);

    module.exports = app;
    
// index.js
    const app = require('./app');
    const PORT = process.env.PORT || 3000;

    app.listen(PORT, () => {
      console.log(`Server kører på port ${PORT}`);
    });
    

5. Express fundamentals

Opret en Express-app

const express = require('express');
    const app = express();
    

Routing

Express matcher HTTP-metode +sti til handler-funktioner:

// GET - Hent alle
    app.get('/api/users', (req, res) => {
      res.json([{ id: 1, navn: 'Lars' }]);
    });

    // GET - Hent én
    app.get('/api/users/:id', (req, res) => {
      const { id } = req.params; // URL-parameter
      res.json({ id, navn: 'Lars' });
    });

    // POST - Opret
    app.post('/api/users', (req, res) => {
      const { navn, email } = req.body; // JSON-body
      res.status(201).json({ id: 2, navn, email });
    });

    // PUT - Opdater
    app.put('/api/users/:id', (req, res) => {
      const { id } = req.params;
      const { navn } = req.body;
      res.json({ id, navn });
    });

    // DELETE - Slet
    app.delete('/api/users/:id', (req, res) => {
      res.status(204).send();
    });
    

Request-objektet (req)

req.params       // URL-parametre: /users/:id
    req.query        // Query-string: /users?page=2&limit=10
    req.body         // JSON-body (kræver middleware)
    req.headers      // HTTP-headers
    req.method       // HTTP-metode (GET, POST, osv.)
    req.url          // Den fulde URL
    req.cookies      // Cookies (med cookie-parser)
    

Response-objektet (res)

res.status(200)         // Sæt HTTP-statuskode
    res.json(data)          // Send JSON-svar
    res.send('Tekst')       // Send tekst
    res.sendFile(path)      // Send en fil
    res.redirect('/login')  // Omdiriger
    res.set('X-Header', 'værdi')  // Sæt header
    res.cookie('navn', 'værdi')   // Sæt cookie
    

Router-middleware

Brug en router til at organisere ruter under en fællessti:

const router = express.Router();

    router.get('/', getAll);
    router.get('/:id', getById);
    router.post('/', create);

    // Brug routeren
    app.use('/api/todos', router);
    

6. REST API design

REST (Representational State Transfer) er en arkitekturstil til at designe APIs. Her er de vigtigste principper:

HTTP-metoder og statuskoder

Metode Formål Statuskode Eksempel
GET Hent data 200 OK GET /api/users
POST Opret ny ressource 201 Created POST /api/users
PUT Erstat ressource helt 200 OK PUT /api/users/1
PATCH Opdater delvist 200 OK PATCH /api/users/1
DELETE Slet ressource 204 No Content DELETE /api/users/1

Fælles statuskoder

2xx Succes
      200 OK                 - forespørgsel lykkedes
      201 Created            - ressource oprettet
      204 No Content         - forespørgsel lykkedes, intet indhold

    3xx Omdirigering
      301 Moved Permanently   - permanent URL-ændring
      304 Not Modified       - cache er stadig gyldig

    4xx Client-fejl
      400 Bad Request        - ugyldigt input
      401 Unauthorized       - ikke logget ind
      403 Forbidden          - ikke tilladt
      404 Not Found          - ressource ikke fundet
      409 Conflict           - konflikt med eksisterende data
      422 Unprocessable      - input gyldigt men logisk fejl

    5xx Server-fejl
      500 Internal Error     - ukendt serverfejl
      502 Bad Gateway        - upstream-tjeneste fejlede
      503 Service Unavailable - serveren er midlertidigt nede
    

API-versionering

Brug version i URL-stien for at undgå at ødelægge eksisterende klienter:

// Version 1
    app.use('/api/v1/users', v1UserRoutes);

    // Version 2
    app.use('/api/v2/users', v2UserRoutes);
    

Eller brug headers:

// Client sender: Accept: application/vnd.myapi.v2+json
    app.get('/api/users', (req, res) => {
      const version = req.headers['accept']?.includes('v2') ? 'v2' : 'v1';
      // ...
    });
    

Pagination

// GET /api/users?page=2&limit=10
    app.get('/api/users', async (req, res) => {
      const page = parseInt(req.query.page) || 1;
      const limit = parseInt(req.query.limit) || 10;
      const skip = (page - 1) * limit;

      const users = await User.find()
        .skip(skip)
        .limit(limit);

      const total = await User.countDocuments();

      res.json({
        data: users,
        pagination: {
          page,
          limit,
          total,
          pages: Math.ceil(total / limit)
        }
      });
    });
    

7. Middleware

Middleware er funktioner der kører før en request når frem til din route-handler. De kan modificere req og res, logge, validere, håndtere fejl osv.

Sådan fungerer kæden

Request → Middleware 1 → Middleware 2 → Route Handler → Response
    

Indbygget middleware

// Parse JSON bodies
    app.use(express.json());

    // Parse URL-encoded bodies (HTML forms)
    app.use(express.urlencoded({ extended: true }));

    // Server statiske filer (CSS, billeder, osv.)
    app.use(express.static('public'));
    

Tredjeparts middleware

npm install cors helmet morgan cookie-parser
    
const cors = require('cors');
    const helmet = require('helmet');
    const morgan = require('morgan');
    const cookieParser = require('cookie-parser');

    // CORS - tillad forespørgsler fra andre domæner
    app.use(cors({
      origin: 'http://localhost:5173', // din frontend
      credentials: true
    }));

    // Helmet - sætter sikkerheds-headers
    app.use(helmet());

    // Morgan - logger requests til konsollen
    app.use(morgan('dev'));

    // Cookie-parser
    app.use(cookieParser());
    

Custom middleware

// Log alle requests
    const logger = (req, res, next) => {
      console.log(`${req.method} ${req.url} - ${new Date().toISOString()}`);
      next(); // VIGTIGT: kal næste middleware/route
    };

    // Tilføj tidspunkt til request
    const addTimestamp = (req, res, next) => {
      req.requestTime = Date.now();
      next();
    };

    app.use(logger);
    app.use(addTimestamp);
    

Middleware specifik for en route

// Kører KUN for denne route
    app.get('/api/data', logger, addTimestamp, (req, res) => {
      res.json({ tid: req.requestTime });
    });
    

Middleware der stopper kæden (authentication)

const authenticate = (req, res, next) => {
      const token = req.headers.authorization?.split(' ')[1];

      if (!token) {
        return res.status(401).json({ error: 'Ingen token' });
      }

      try {
        const decoded = jwt.verify(token, process.env.JWT_SECRET);
        req.bruger = decoded;
        next(); // Token gyldig - fortsæt
      } catch (err) {
        return res.status(401).json({ error: 'Ugyldig token' });
      }
    };

    // Brug den
    app.get('/api/profil', authenticate, (req, res) => {
      res.json({ bruger: req.bruger });
    });
    

8. Database

Valg: NoSQL vs SQL

MongoDB (NoSQL) PostgreSQL (SQL)
Data model Dokumenter (JSON-lignende) Tabeller med rækker og kolonner
Schema Fleksibel (schema-less) Stærkt schema
Bedst til Prototyper, hurtig udvikling Komplekse relationer, dataintegritet
Skalering Horisontal (sharding) Vertikal (primært)
Opmærksomhed Ingen joins Joins er naturlige

MongoDB + Mongoose

npm install mongoose
    
// config/database.js
    const mongoose = require('mongoose');

    const connectDB = async () => {
      try {
        await mongoose.connect(process.env.MONGO_URI);
        console.log('MongoDB forbundet');
      } catch (err) {
        console.error('Databasefejl:', err.message);
        process.exit(1);
      }
    };

    module.exports = connectDB;
    
// models/User.js
    const mongoose = require('mongoose');

    const userSchema = new mongoose.Schema({
      navn: {
        type: String,
        required: [true, 'Navn er påkrævet'],
        trim: true,
        minlength: 2,
        maxlength: 50
      },
      email: {
        type: String,
        required: [true, 'Email er påkrævet'],
        unique: true,
        lowercase: true,
        match: [/^\S+@\S+\.\S+$/, 'Ugyldig email']
      },
      kodeord: {
        type: String,
        required: true,
        minlength: 6,
        select: false // Ekskludér fra queries som standard
      },
      rolle: {
        type: String,
        enum: ['bruger', 'admin'],
        default: 'bruger'
      }
    }, {
      timestamps: true // Tilføjer createdAt og updatedAt
    });

    module.exports = mongoose.model('User', userSchema);
    
// Brug modellen i controller
    const User = require('../models/User');

    const getAllUsers = async (req, res) => {
      const users = await User.find().select('-__v');
      res.json(users);
    };

    const createUser = async (req, res) => {
      const user = await User.create(req.body);
      res.status(201).json(user);
    };
    

PostgreSQL + Prisma

npm install prisma @prisma/client
    npx prisma init
    
// prisma/schema.prisma
    generator client {
      provider = "prisma-client-js"
    }

    datasource db {
      provider = "postgresql"
      url      = env("DATABASE_URL")
    }

    model User {
      id        Int      @id @default(autoincrement())
      navn      String
      email     String   @unique
      kodeord   String
      todos     Todo[]
      createdAt DateTime @default(now())
      updatedAt DateTime @updatedAt
    }

    model Todo {
      id          Int      @id @default(autoincrement())
      titel       String
      beskrivelse String?
      færdig      Boolean  @default(false)
      bruger      User     @relation(fields: [brugerId], references: [id])
      brugerId    Int
      createdAt   DateTime @default(now())
      updatedAt   DateTime @updatedAt
    }
    
npx prisma db push    # Push schema til database
    npx prisma generate  # Generer klienten
    
// Brug Prisma-klienten
    const { PrismaClient } = require('@prisma/client');
    const prisma = new PrismaClient();

    const getAllTodos = async (req, res) => {
      const todos = await prisma.todo.findMany({
        where: { brugerId: req.bruger.id },
        include: { bruger: { select: { navn: true, email: true } } }
      });
      res.json(todos);
    };

    const createTodo = async (req, res) => {
      const todo = await prisma.todo.create({
        data: {
          titel: req.body.titel,
          brugerId: req.bruger.id
        }
      });
      res.status(201).json(todo);
    };
    

9. Validering

Aldrig stole på input fra klienten. Valider altid.

express-validator

npm install express-validator
    
// middleware/validate.js
    const { body, param, validationResult } = require('express-validator');

    const handleErrors = (req, res, next) => {
      const errors = validationResult(req);
      if (!errors.isEmpty()) {
        return res.status(400).json({ errors: errors.array() });
      }
      next();
    };

    const validateBruger = [
      body('navn')
        .trim()
        .isLength({ min: 2, max: 50 })
        .withMessage('Navn skal være mellem 2 og 50 tegn'),
      body('email')
        .isEmail()
        .withMessage('Ugyldig email-adresse')
        .normalizeEmail(),
      body('kodeord')
        .isLength({ min: 6 })
        .withMessage('Kodeord skal være mindst 6 tegn'),
      handleErrors
    ];

    const validateId = [
      param('id')
        .isMongoId()
        .withMessage('Ugyldig ID'),
      handleErrors
    ];

    module.exports = { validateBruger, validateId };
    
// Brug i route
    const { validateBruger } = require('../middleware/validate');

    router.post('/', validateBruger, controller.create);
    

Joi

npm install joi
    
const Joi = require('joi');

    const brugerSchema = Joi.object({
      navn: Joi.string().min(2).max(50).required(),
      email: Joi.string().email().required(),
      kodeord: Joi.string().min(6).required(),
      rolle: Joi.string().valid('bruger', 'admin').default('bruger')
    });

    // Middleware
    const validate = (schema) => (req, res, next) => {
      const { error } = schema.validate(req.body);
      if (error) {
        return res.status(400).json({
          error: error.details.map(d => d.message)
        });
      }
      next();
    };

    // Brug den
    app.post('/api/brugere', validate(brugerSchema), controller.create);
    

Input-sanitering

Fjern potentielt farlige tegn fra input:

const sanitizeHtml = require('sanitize-html');

    const sanitize = (req, res, next) => {
      if (req.body.titel) {
        req.body.titel = sanitizeHtml(req.body.titel, {
          allowedTags: [], // Ingen HTML-tilladt
          allowedAttributes: {}
        });
      }
      next();
    };
    

10. Fejlhåndtering

Grundlæggende mønster

// middleware/errorHandler.js
    const errorHandler = (err, req, res, next) => {
      console.error(err.stack);

      const status = err.statusCode || 500;
      const message = err.message || 'Intern serverfejl';

      res.status(status).json({
        error: {
          message,
          status,
          ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
        }
      });
    };

    module.exports = errorHandler;
    

Brug den globale handler

const errorHandler = require('./middleware/errorHandler');

    // Alle routes først...
    app.use('/api', routes);

    // Fejlhåndtering til sidst (vigtig rækkefølge!)
    app.use(errorHandler);
    

Custom fejl-klasse

// utils/AppError.js
    class AppError extends Error {
      constructor(message, statusCode) {
        super(message);
        this.statusCode = statusCode;
        this.isOperational = true;
        Error.captureStackTrace(this, this.constructor);
      }
    }

    module.exports = AppError;
    
// Brug den i controller
    const AppError = require('../utils/AppError');

    const getUser = async (req, res, next) => {
      try {
        const user = await User.findById(req.params.id);

        if (!user) {
          throw new AppError('Bruger ikke fundet', 404);
        }

        res.json(user);
      } catch (err) {
        next(err); // Videregiv fejl til Express
      }
    };
    

Async fejlhåndtering

Undgå at gentage try/catch i hver controller:

// utils/catchAsync.js
    const catchAsync = (fn) => (req, res, next) => {
      fn(req, res, next).catch(next);
    };

    module.exports = catchAsync;
    
// Nu kan du skrive:
    const getAllUsers = catchAsync(async (req, res) => {
      const users = await User.find();
      res.json(users);
    });
    

Valideringsfejl

// Express valideringsfejl-håndtering
    app.use((err, req, res, next) => {
      if (err.type === 'entity.parse.failed') {
        return res.status(400).json({ error: 'Ugyldig JSON i request body' });
      }
      next(err);
    });
    

11. Autentificering

JWT (JSON Web Tokens)

npm install jsonwebtoken bcryptjs
    

Brugerregistrering med hash af kodeord

const bcrypt = require('bcryptjs');
    const jwt = require('jsonwebtoken');

    const register = async (req, res) => {
      const { navn, email, kodeord } = req.body;

      // Tjek om bruger allerede eksisterer
      const existingUser = await User.findOne({ email });
      if (existingUser) {
        return res.status(409).json({ error: 'Email er allerede i brug' });
      }

      // Hash kodeordet
      const salt = await bcrypt.genSalt(10);
      const hashedPassword = await bcrypt.hash(kodeord, salt);

      // Opret bruger
      const user = await User.create({
        navn,
        email,
        kodeord: hashedPassword
      });

      // Generer token
      const token = jwt.sign(
        { id: user._id, rolle: user.rolle },
        process.env.JWT_SECRET,
        { expiresIn: '7d' }
      );

      res.status(201).json({
        token,
        bruger: { id: user._id, navn: user.navn, email: user.email }
      });
    };
    

Login

const login = async (req, res) => {
      const { email, kodeord } = req.body;

      // Find bruger og inkludér kodeord-feltet
      const user = await User.findOne({ email }).select('+kodeord');

      if (!user || !(await bcrypt.compare(kodeord, user.kodeord))) {
        return res.status(401).json({ error: 'Forkert email eller kodeord' });
      }

      const token = jwt.sign(
        { id: user._id, rolle: user.rolle },
        process.env.JWT_SECRET,
        { expiresIn: '7d' }
      );

      res.json({
        token,
        bruger: { id: user._id, navn: user.navn, email: user.email }
      });
    };
    

Refresh tokens

En refresh token er en langsigtet token der bruges til at få nye access tokens:

const generateTokens = (user) => {
      const accessToken = jwt.sign(
        { id: user._id },
        process.env.JWT_SECRET,
        { expiresIn: '15m' }
      );

      const refreshToken = jwt.sign(
        { id: user._id },
        process.env.JWT_REFRESH_SECRET,
        { expiresIn: '7d' }
      );

      return { accessToken, refreshToken };
    };

    // Endpoint til at forny access token
    const refreshAccessToken = async (req, res) => {
      const { refreshToken } = req.body;

      if (!refreshToken) {
        return res.status(401).json({ error: 'Ingen refresh token' });
      }

      try {
        const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
        const user = await User.findById(decoded.id);

        if (!user) {
          return res.status(401).json({ error: 'Bruger ikke fundet' });
        }

        const tokens = generateTokens(user);
        res.json(tokens);
      } catch (err) {
        return res.status(403).json({ error: 'Ugyldig refresh token' });
      }
    };
    

Beskyt routes

// middleware/auth.js
    const authenticate = (req, res, next) => {
      const authHeader = req.headers.authorization;

      if (!authHeader?.startsWith('Bearer ')) {
        return res.status(401).json({ error: 'Ingen token' });
      }

      const token = authHeader.split(' ')[1];

      try {
        const decoded = jwt.verify(token, process.env.JWT_SECRET);
        req.bruger = decoded;
        next();
      } catch (err) {
        return res.status(401).json({ error: 'Token er ugyldig eller udløbet' });
      }
    };

    const authorize = (...roller) => (req, res, next) => {
      if (!roller.includes(req.bruger.rolle)) {
        return res.status(403).json({ error: 'Ingen adgang' });
      }
      next();
    };

    module.exports = { authenticate, authorize };
    
// Brug det
    router.get('/admin/brugere', authenticate, authorize('admin'), adminController.getAll);
    router.get('/profil', authenticate, profileController.get);
    

12. Filoploading

Multer

npm install multer
    
const multer = require('multer');
    const path = require('path');

    // Gem filer lokalt
    const storage = multer.diskStorage({
      destination: (req, file, cb) => {
        cb(null, 'uploads/');
      },
      filename: (req, file, cb) => {
        const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
        cb(null, `${uniqueName}${path.extname(file.originalname)}`);
      }
    });

    // Filfilter
    const fileFilter = (req, file, cb) => {
      const allowed = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf'];
      if (allowed.includes(file.mimetype)) {
        cb(null, true);
      } else {
        cb(new Error('Filtypen er ikke tilladt'), false);
      }
    };

    const upload = multer({
      storage,
      fileFilter,
      limits: { fileSize: 5 * 1024 * 1024 } // 5MB max
    });

    // Brug i route
    router.post('/upload', upload.single('billede'), (req, res) => {
      res.json({
        fil: req.file.filename,
        sti: `/uploads/${req.file.filename}`
      });
    });

    // Multiple filer
    router.post('/uploads', upload.array('billeder', 5), (req, res) => {
      res.json({ filer: req.files.map(f => f.filename) });
    });
    

Cloud storage (S3 / Cloudflare R2)

npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
    
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
    const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');

    const s3 = new S3Client({
      region: 'auto',
      endpoint: process.env.R2_ENDPOINT,
      credentials: {
        accessKeyId: process.env.R2_ACCESS_KEY,
        secretAccessKey: process.env.R2_SECRET_KEY
      }
    });

    const uploadTilR2 = async (req, res) => {
      const filnavn = `${Date.now()}-${req.file.originalname}`;

      const command = new PutObjectCommand({
        Bucket: process.env.R2_BUCKET,
        Key: filnavn,
        Body: req.file.buffer,
        ContentType: req.file.mimetype
      });

      await s3.send(command);

      const url = `${process.env.R2_PUBLIC_URL}/${filnavn}`;
      res.json({ url });
    };
    

Multer med memory storage (til cloud upload)

const uploadMemory = multer({
      storage: multer.memoryStorage(),
      limits: { fileSize: 10 * 1024 * 1024 } // 10MB
    });

    router.post('/upload-cloud', uploadMemory.single('fil'), uploadTilR2);
    

13. Real-time

Socket.io

npm install socket.io
    
// server.js
    const express = require('express');
    const http = require('http');
    const { Server } = require('socket.io');
    const jwt = require('jsonwebtoken');

    const app = express();
    const server = http.createServer(app);
    const io = new Server(server, {
      cors: {
        origin: 'http://localhost:5173',
        methods: ['GET', 'POST']
      }
    });

    // Socket.io middleware (auth)
    io.use((socket, next) => {
      const token = socket.handshake.auth.token;
      try {
        const decoded = jwt.verify(token, process.env.JWT_SECRET);
        socket.bruger = decoded;
        next();
      } catch (err) {
        next(new Error('Autentificering fejlede'));
      }
    });

    // Håndter forbindelser
    io.on('connection', (socket) => {
      console.log(`Bruger ${socket.bruger.id} forbundet`);

      // Rum-baseret chat
      socket.on('join-rum', (rumId) => {
        socket.join(rumId);
        socket.to(rumId).emit('besked', {
          type: 'system',
          tekst: `${socket.bruger.id} er kommet med`
        });
      });

      // Modtag og send besked
      socket.on('ny-besked', (data) => {
        io.to(data.rumId).emit('besked', {
          fra: socket.bruger.id,
          tekst: data.tekst,
          tidspunkt: new Date().toISOString()
        });
      });

      // Real-time notifikationer
      socket.on('send-notifikation', (data) => {
        io.to(data.modtagerId).emit('notifikation', {
          fra: socket.bruger.id,
          type: data.type,
          besked: data.besked
        });
      });

      socket.on('disconnect', () => {
        console.log(`Bruger ${socket.bruger.id} afbrudt`);
      });
    });

    server.listen(3000);
    

Klient-side eksempel (JavaScript)

import { io } from 'socket.io-client';

    const socket = io('http://localhost:3000', {
      auth: { token: localStorage.getItem('token') }
    });

    socket.on('connect', () => {
      console.log('Tilsluttet!');
    });

    socket.on('besked', (besked) => {
      console.log(`${besked.fra}: ${besked.tekst}`);
    });

    // Send besked
    socket.emit('ny-besked', {
      rumId: 'general',
      tekst: 'Hej alle sammen!'
    });
    

Live opdateringer (f.eks. opgavetavle)

// Server: Når en opdatering sker
    const updateTodo = async (req, res) => {
      const todo = await Todo.findByIdAndUpdate(
        req.params.id,
        { færdig: req.body.færdig },
        { new: true }
      );

      // Broadcast til alle forbundne klienter
      io.emit('todo-opdatering', {
        action: 'opdateret',
        data: todo
      });

      res.json(todo);
    };
    

14. Deployment

Railway (gratis tier)

  1. Opret en konto på railway.app
  2. Tilslut dit GitHub-repo
  3. Railway genkender automatisk Node.js
  4. Tilføj miljøvariabler i dashboard
  5. Deploy sker automatisk ved push til main
// railway genkender dette automatisk
    {
      "scripts": {
        "start": "node index.js"
      }
    }
    

Render (gratis tier)

  1. Opret en konto på render.com
  2. Opret en "New Web Service"
  3. Tilslut GitHub-repo
  4. Build command: npm install
  5. Start command: node index.js

Docker

# Dockerfile
    FROM node:20-alpine

    WORKDIR /app

    COPY package*.json ./
    RUN npm ci --only=production

    COPY . .

    EXPOSE 3000

    USER node

    CMD ["node", "index.js"]
    
# docker-compose.yml
    version: '3.8'

    services:
      app:
        build: .
        ports:
          - "3000:3000"
        environment:
          - NODE_ENV=production
          - MONGO_URI=mongodb://mongo:27017/mydb
          - JWT_SECRET=dit-hemmelige-nøgle
        depends_on:
          - mongo

      mongo:
        image: mongo:7
        volumes:
          - mongo-data:/data/db

    volumes:
      mongo-data:
    
# Byg og kør
    docker-compose up -d --build

    # Se logs
    docker-compose logs -f app
    

Miljøvariabler

Opret en .env fil (aldrig i git):

NODE_ENV=production
    PORT=3000
    MONGO_URI=mongodb+srv://bruger:kodeord@cluster.mongodb.net/mydb
    JWT_SECRET=et-meget-langt-tilfældigt-tegnstræk
    JWT_REFRESH_SECRET=et-andet-meget-langt-tegnstræk
    R2_ENDPOINT=https://xxx.r2.cloudflarestorage.com
    R2_ACCESS_KEY=din-nøgle
    R2_SECRET_KEY=din-hemmelige-nøgle
    R2_BUCKET=mit-bucket
    R2_PUBLIC_URL=https://pub-xxx.r2.dev
    

Tilføj .env til .gitignore:

.env
    .env.local
    node_modules/
    

15. Pris

Hvad er gratis?

Ressource Gratis mulighed
Node.js og Express 100% gratis, open source
MongoDB MongoDB Atlas free tier (512 MB)
PostgreSQL Supabase free tier (500 MB), Neon free tier
Hosting Railway ($5 credit/måned), Render (begrænset), Vercel
Cloud storage Cloudflare R2 (10 GB gratis)
Domæne Ikke gratis, men .dev koster ~$12/år

Hvad koster penge?

Ressource Pris
Cloud hosting (VPS) $5-20/måned (DigitalOcean, Hetzner)
MongoDB Atlas Fra $0/måned (free tier) til $57+/måned
Domæne ~$10-15/år
SSL-certifikat Gratis med Let's Encrypt
Email-service (SendGrid) Gratis op til 100 mails/dag
Monitoring (Sentry) Gratis op til 5.000 fejl/måned

Gratis stack til at komme i gang

Backend:     Node.js + Express (gratis)
    Database:    MongoDB Atlas free tier (gratis)
    Hosting:     Railway (gratis med $5 credit)
    Auth:        JWT (gratis)
    Filopload:   Cloudflare R2 (gratis 10 GB)
    Frontend:    Vercel (gratis)
    Domæne:      Ikke nødvendigt til test

    Total:       $0/måned
    

16. Fordele og ulemper

Node.js + Express

Fordele Ulemper
JavaScript overalt (frontend + backend) CPU-tunge opgaver blokerer event loop
Massivt økosystem (npm) Callback-helvede (mindre med async/await)
Hurtig udvikling Dårlig til tung beregning
Stor community og support Dependancy-helvede kan opstå
God til real-time Fejlfinding kan være svært
Let at deploye Ikke det bedste til komplekse domænemodeller

Sammenligning med alternativer

Node.js Django (Python) Go Spring (Java)
Sprog JavaScript Python Go Java
Hastighed God (I/O) Langsom Meget hurtig Hurtig
Udviklingshastighed Meget hurtig Hurtig Langsom Langsom
Læringskurve Nem Nem Medium Svær
Egnet til APIs, real-time Web apps, ML Systemer, APIs Store virksomheder
Community Stort Stort Voksende Stort
Typing Svag (TypeScript hjælper) Svag Stærk Stærk
Deployment Nem Nem Nem Svær

Hvornår vælge hvad?


17. Eksempel-projekt: Todo API med auth

Her bygger vi et komplet Todo API med brugerregistrering, login og CRUD-operationer.

Setup

mkdir todo-api && cd todo-api
    npm init -y
    npm install express mongoose bcryptjs jsonwebtoken cors helmet dotenv
    npm install -D nodemon
    

Projektstruktur

todo-api/
    ├── src/
    │   ├── config/
    │   │   └── db.js
    │   ├── middleware/
    │   │   ├── auth.js
    │   │   ├── errorHandler.js
    │   │   └── validate.js
    │   ├── models/
    │   │   ├── User.js
    │   │   └── Todo.js
    │   ├── routes/
    │   │   ├── auth.routes.js
    │   │   └── todo.routes.js
    │   ├── controllers/
    │   │   ├── auth.controller.js
    │   │   └── todo.controller.js
    │   └── app.js
    ├── .env
    ├── .gitignore
    ├── package.json
    └── index.js
    

Filer

# .env
    PORT=3000
    MONGO_URI=mongodb://localhost:27017/todo-api
    JWT_SECRET=min-hemmelige-nøgle-12345
    NODE_ENV=development
    
# .gitignore
    node_modules/
    .env
    
// src/config/db.js
    const mongoose = require('mongoose');

    const connectDB = async () => {
      try {
        const conn = await mongoose.connect(process.env.MONGO_URI);
        console.log(`MongoDB forbundet: ${conn.connection.host}`);
      } catch (err) {
        console.error(`Databasefejl: ${err.message}`);
        process.exit(1);
      }
    };

    module.exports = connectDB;
    
// src/models/User.js
    const mongoose = require('mongoose');
    const bcrypt = require('bcryptjs');

    const userSchema = new mongoose.Schema({
      navn: {
        type: String,
        required: [true, 'Navn er påkrævet'],
        trim: true
      },
      email: {
        type: String,
        required: [true, 'Email er påkrævet'],
        unique: true,
        lowercase: true
      },
      kodeord: {
        type: String,
        required: true,
        minlength: 6,
        select: false
      }
    }, { timestamps: true });

    userSchema.pre('save', async function (next) {
      if (!this.isModified('kodeord')) return next();
      this.kodeord = await bcrypt.hash(this.kodeord, 10);
      next();
    });

    userSchema.methods.sammenlignKodeord = async function (kandidatKodeord) {
      return await bcrypt.compare(kandidatKodeord, this.kodeord);
    };

    module.exports = mongoose.model('User', userSchema);
    
// src/models/Todo.js
    const mongoose = require('mongoose');

    const todoSchema = new mongoose.Schema({
      titel: {
        type: String,
        required: [true, 'Titel er påkrævet'],
        trim: true,
        maxlength: 100
      },
      beskrivelse: {
        type: String,
        default: ''
      },
      færdig: {
        type: Boolean,
        default: false
      },
      bruger: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'User',
        required: true
      }
    }, { timestamps: true });

    module.exports = mongoose.model('Todo', todoSchema);
    
// src/middleware/auth.js
    const jwt = require('jsonwebtoken');

    const authenticate = (req, res, next) => {
      const authHeader = req.headers.authorization;

      if (!authHeader?.startsWith('Bearer ')) {
        return res.status(401).json({ error: 'Adgang nægtet - ingen token' });
      }

      const token = authHeader.split(' ')[1];

      try {
        const decoded = jwt.verify(token, process.env.JWT_SECRET);
        req.bruger = decoded;
        next();
      } catch (err) {
        return res.status(401).json({ error: 'Token er ugyldig' });
      }
    };

    module.exports = { authenticate };
    
// src/middleware/errorHandler.js
    const errorHandler = (err, req, res, next) => {
      console.error(err.stack);

      const status = err.statusCode || 500;
      const message = err.message || 'Intern serverfejl';

      res.status(status).json({ error: message });
    };

    module.exports = errorHandler;
    
// src/middleware/validate.js
    const { body, validationResult } = require('express-validator');

    const handleErrors = (req, res, next) => {
      const errors = validationResult(req);
      if (!errors.isEmpty()) {
        return res.status(400).json({ errors: errors.array() });
      }
      next();
    };

    const validateRegistrering = [
      body('navn').trim().notEmpty().withMessage('Navn er påkrævet'),
      body('email').isEmail().withMessage('Ugyldig email'),
      body('kodeord').isLength({ min: 6 }).withMessage('Kodeord skal være mindst 6 tegn'),
      handleErrors
    ];

    const validateLogin = [
      body('email').isEmail().withMessage('Ugyldig email'),
      body('kodeord').notEmpty().withMessage('Kodeord er påkrævet'),
      handleErrors
    ];

    const validateTodo = [
      body('titel').trim().notEmpty().withMessage('Titel er påkrævet'),
      handleErrors
    ];

    module.exports = { validateRegistrering, validateLogin, validateTodo };
    
// src/controllers/auth.controller.js
    const User = require('../models/User');
    const jwt = require('jsonwebtoken');

    const genererToken = (bruger) => {
      return jwt.sign(
        { id: bruger._id, navn: bruger.navn },
        process.env.JWT_SECRET,
        { expiresIn: '7d' }
      );
    };

    exports.registrer = async (req, res) => {
      const { navn, email, kodeord } = req.body;

      const eksisterende = await User.findOne({ email });
      if (eksisterende) {
        return res.status(409).json({ error: 'Email er allerede i brug' });
      }

      const bruger = await User.create({ navn, email, kodeord });
      const token = genererToken(bruger);

      res.status(201).json({
        token,
        bruger: { id: bruger._id, navn: bruger.navn, email: bruger.email }
      });
    };

    exports.login = async (req, res) => {
      const { email, kodeord } = req.body;

      const bruger = await User.findOne({ email }).select('+kodeord');
      if (!bruger || !(await bruger.sammenlignKodeord(kodeord))) {
        return res.status(401).json({ error: 'Forkert email eller kodeord' });
      }

      const token = genererToken(bruger);

      res.json({
        token,
        bruger: { id: bruger._id, navn: bruger.navn, email: bruger.email }
      });
    };

    exports.profil = async (req, res) => {
      const bruger = await User.findById(req.bruger.id);
      res.json({ bruger });
    };
    
// src/controllers/todo.controller.js
    const Todo = require('../models/Todo');

    exports.getAll = async (req, res) => {
      const todos = await Todo.find({ bruger: req.bruger.id })
        .sort({ createdAt: -1 });
      res.json(todos);
    };

    exports.getById = async (req, res) => {
      const todo = await Todo.findOne({
        _id: req.params.id,
        bruger: req.bruger.id
      });

      if (!todo) {
        return res.status(404).json({ error: 'Todo ikke fundet' });
      }

      res.json(todo);
    };

    exports.create = async (req, res) => {
      const todo = await Todo.create({
        titel: req.body.titel,
        beskrivelse: req.body.beskrivelse || '',
        bruger: req.bruger.id
      });

      res.status(201).json(todo);
    };

    exports.update = async (req, res) => {
      const todo = await Todo.findOneAndUpdate(
        { _id: req.params.id, bruger: req.bruger.id },
        req.body,
        { new: true, runValidators: true }
      );

      if (!todo) {
        return res.status(404).json({ error: 'Todo ikke fundet' });
      }

      res.json(todo);
    };

    exports.delete = async (req, res) => {
      const todo = await Todo.findOneAndDelete({
        _id: req.params.id,
        bruger: req.bruger.id
      });

      if (!todo) {
        return res.status(404).json({ error: 'Todo ikke fundet' });
      }

      res.status(204).send();
    };
    
// src/routes/auth.routes.js
    const express = require('express');
    const router = express.Router();
    const authController = require('../controllers/auth.controller');
    const { validateRegistrering, validateLogin } = require('../middleware/validate');
    const { authenticate } = require('../middleware/auth');

    router.post('/registrer', validateRegistrering, authController.registrer);
    router.post('/login', validateLogin, authController.login);
    router.get('/profil', authenticate, authController.profil);

    module.exports = router;
    
// src/routes/todo.routes.js
    const express = require('express');
    const router = express.Router();
    const todoController = require('../controllers/todo.controller');
    const { authenticate } = require('../middleware/auth');
    const { validateTodo } = require('../middleware/validate');

    router.use(authenticate); // Alle todo-ruter kræver login

    router.get('/', todoController.getAll);
    router.get('/:id', todoController.getById);
    router.post('/', validateTodo, todoController.create);
    router.put('/:id', validateTodo, todoController.update);
    router.delete('/:id', todoController.delete);

    module.exports = router;
    
// src/app.js
    const express = require('express');
    const cors = require('cors');
    const helmet = require('helmet');
    const authRoutes = require('./routes/auth.routes');
    const todoRoutes = require('./routes/todo.routes');
    const errorHandler = require('./middleware/errorHandler');

    const app = express();

    // Middleware
    app.use(helmet());
    app.use(cors());
    app.use(express.json());

    // Routes
    app.get('/', (req, res) => {
      res.json({ message: 'Todo API kører!' });
    });

    app.use('/api/auth', authRoutes);
    app.use('/api/todos', todoRoutes);

    // 404-håndtering
    app.use((req, res) => {
      res.status(404).json({ error: 'Route ikke fundet' });
    });

    // Fejlhåndtering
    app.use(errorHandler);

    module.exports = app;
    
// index.js
    require('dotenv').config();
    const app = require('./src/app');
    const connectDB = require('./src/config/db');

    const PORT = process.env.PORT || 3000;

    connectDB().then(() => {
      app.listen(PORT, () => {
        console.log(`Server kører på http://localhost:${PORT}`);
      });
    });
    

Test API'et

npm run dev
    
# Registrer en bruger
    curl -X POST http://localhost:3000/api/auth/registrer \
      -H "Content-Type: application/json" \
      -d '{"navn":"Lars","email":"lars@test.dk","kodeord":"123456"}'

    # Login
    curl -X POST http://localhost:3000/api/auth/login \
      -H "Content-Type: application/json" \
      -d '{"email":"lars@test.dk","kodeord":"123456"}'

    # Opret en todo (brug token fra login)
    curl -X POST http://localhost:3000/api/todos \
      -H "Content-Type: application/json" \
      -H "Authorization: Bearer DIN_TOKEN" \
      -d '{"titel":"Købe mælk"}'

    # Hent alle todos
    curl http://localhost:3000/api/todos \
      -H "Authorization: Bearer DIN_TOKEN"

    # Opdater en todo
    curl -X PUT http://localhost:3000/api/todos/TODO_ID \
      -H "Content-Type: application/json" \
      -H "Authorization: Bearer DIN_TOKEN" \
      -d '{"færdig":true}'

    # Slet en todo
    curl -X DELETE http://localhost:3000/api/todos/TODO_ID \
      -H "Authorization: Bearer DIN_TOKEN"
    

Opsamling

Du har nu lært at:

Næste skridt


Guide skrevet i juni 2026. Tjek altid den nyeste dokumentation for de brugte biblioteker.