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
- Hvad er Node.js?
- Hvornår skal du bruge det?
- Kom i gang
- Projektstruktur
- Express fundamentals
- REST API design
- Middleware
- Database
- Validering
- Fejlhåndtering
- Autentificering
- Filoploading
- Real-time
- Deployment
- Pris
- Fordele og ulemper
- 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
- Event-driven og non-blocking I/O: Node.js bruger et event loop i stedet for tråde. Det gør det meget effektivt til I/O-tunge operationer som at læse filer, forespørge databaser eller håndtere mange samtidige netværksforbindelser.
- Single-threaded: Alt kører på én hovedtråd, men asynkron kode gør at det alligevel kan håndtere tusindvis af forespørgsler samtidigt.
- npm: Node Package Manager — verdens største økosystem af open source-pakker. Over 2 millioner pakker tilgængelige.
Hvem bruger Node.js?
| Virksomhed | Hvad de bruger det til |
|---|---|
| Netflix | Server-side rendering, API gateway |
| Uber | Microservices, real-time data |
| 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)
- Opret en konto på railway.app
- Tilslut dit GitHub-repo
- Railway genkender automatisk Node.js
- Tilføj miljøvariabler i dashboard
- Deploy sker automatisk ved push til
main
// railway genkender dette automatisk
{
"scripts": {
"start": "node index.js"
}
}
Render (gratis tier)
- Opret en konto på render.com
- Opret en "New Web Service"
- Tilslut GitHub-repo
- Build command:
npm install - 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?
- Node.js: Hurtige APIs, real-time, prototyper, JavaScript-teams
- Django: Dataintensive web apps, ML-integration, admin-panels
- Go: Høj ydeevne, mikroservices, DevOps-værktøjer
- Spring: Store virksomheder, komplekse domænemodeller, compliance
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:
- Forstå hvad Node.js er og hvornår det bruges
- Sætte et Express-projekt op fra bunden
- Strukturere et projekt med routes, controllers og middleware
- Designe et REST API med korrekte metoder og statuskoder
- Håndtere middleware-kæder
- Arbejde med MongoDB (Mongoose) og PostgreSQL (Prisma)
- Validere input og håndtere fejl
- Implementere JWT-autentificering
- Uploade filer lokalt og til cloud
- Bygge real-time features med Socket.io
- Deploye til Railway, Render eller Docker
- Forstå omkostninger og sammenligne med alternativer
- Bygge et komplet Todo API fra bunden
Næste skridt
- Tilføj tests med Jest eller Vitest
- Implementér rate limiting med express-rate-limit
- Tilføj logging med Winston eller Pino
- Brug TypeScript for bedre type-sikkerhed
- Udforsk GraphQL med Apollo Server
- Prøv NestJS for et mere struktureret framework
Guide skrevet i juni 2026. Tjek altid den nyeste dokumentation for de brugte biblioteker.