Workstation Logo
Producten
AI LabsOpenAI AgentsClaude AgentsGrok BotWorkstation CRM (WSL CRM)MarketingAlle Producten
AI-Oplossingen
AI-WerkstationsAI SME PackagesPrivé AIGPU-ClustersEdge AIEnterprise AI LabAI per Industrie
Diensten
PlatformmoderniseringDigitale engineeringDatafundamenten & AIAutonome operatiesAI-adviesDevOps-automatiseringCybersecuritySoftwareontwikkelingAgentontwikkelingMLOps-opzet
Over Ons
PartnersKlantverhalen
Artikelen
Documentatie
WSL ProxyRing PromoterWSL VaultJobshoutSysOps 24/7
Blog
ContactLogin
Workstation

AI-werkstations, AI multi-agent software, GPU-infrastructuur en intelligente agentoplossingen voor moderne bedrijven.

Contact

AI-oplossingen

AI-WerkstationsAI SME PackagesPrivé AIGPU-ClustersEdge AIEnterprise AI LabAI per Industrie

Producten

Alle ProductenWSL CRM & ERPMarketingOpenAI AgentsWSL ProxyRing PromoterWSL VaultJobshoutSysOps 24/7

Bedrijf

Over OnsWaarom WorkstationPartnersKlantverhalenPrijzenContact

Bronnen

ArtikelenDocumentatieBlogZoekenSitemap
UK-kantoor
77-79 Marlowes, Hemel Hempstead HP1 1LFRoute: neem afrit 20 van de M25, Outer LondonBedrijfsnummer: 11641870Ma - Vr: 9:00 - 18:00 GMT
+44 7515 356 146
België-kantoor
Workstation SRL, Rue Vanderkindere 34, 1180 Uccle, BrusselsBE 0751.518.683Ma - Vr: 9:00 - 18:00 CET
+32 492 45 67 46
India-kantoor
#159 Sector 9, Pocket 1, DDA Flats, 110077 Dwarka, New Delhi
+91 98881 98841

© 2026 Workstation AI. Alle rechten voorbehouden.

PrivacyCookiesServicevoorwaardenWebsite-sitemap

Loading blog...

Home / Blog
WebFrontendReact

Robuuste backend-systemen bouwen met Node.js en MySQL

Ontwerp en implementeer schaalbare, veilige backend API's met Node.js, Express en MySQL

Balinder Walia27 mei 202511 min read

Node.js en MySQL blijven een van de meest betrouwbare en beproefde combinaties voor het bouwen van backend-systemen. Node.js biedt een gebeurtenisgestuurd, niet-blokkerend I/O-model dat gelijktijdige verzoeken efficiënt afhandelt, terwijl MySQL de relationele gegevensintegriteit levert waar bedrijfsapplicaties om vragen. Samen vormen ze een stichting die alles aanstuurt, van startup MVP's tot ondernemingsplatforms die miljoenen verzoeken per dag verwerken.

In deze handleiding wordt uitgelegd hoe u een backend API op productieniveau bouwt, waarbij de projectstructuur, databaseontwerp, authenticatie, foutafhandeling en implementatie aan bod komen.

Express.js Projectstructuur

Node.js + Express Backend-architectuurMiddleware-laagAuth (JWT)ValidatieSnelheidslimietCORSHelmetLogboekregistratieClientBrowser / AppAPIGateway/api/v1ExpressRoutesGET/POST/PUT/DELControllersVerzoekafhandelingServicesBedrijfslogicaMySQLRelationele DBHTTP-routeoproepoproepqueryAntwoordformaat{ succes: waar,gegevens: [...],paginering: {...}}Pijplijn voor foutafhandelingApiErrorasyncHandlererrorHandlerGecentraliseerde, consistente foutreacties

Een goed georganiseerde projectstructuur is de basis van een onderhoudbare backend. Scheid de zorgen duidelijk van elkaar en leg vroegtijdig afspraken vast.

project-root/
  src/
    config/
      database.js
      environment.js
      logger.js
    middleware/
      auth.js
      errorHandler.js
      rateLimiter.js
      validator.js
    models/
      User.js
      Product.js
      Order.js
      index.js
    routes/
      auth.routes.js
      users.routes.js
      products.routes.js
      orders.routes.js
      index.js
    services/
      auth.service.js
      user.service.js
      product.service.js
      email.service.js
    utils/
      ApiError.js
      asyncHandler.js
      pagination.js
    app.js
    server.js
  migrations/
  seeders/
  tests/
  .env
  .env.example
  package.json

Het toegangspunt installeert Express met essentiële middleware:

// src/app.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const { errorHandler } = require('./middleware/errorHandler');
const routes = require('./routes');

const app = express();

// Security middleware
app.use(helmet());
app.use(cors({
  origin: process.env.ALLOWED_ORIGINS?.split(',') || 'http://localhost:3000',
  credentials: true,
}));

// Request parsing
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));

// Logging
app.use(morgan(process.env.NODE_ENV === 'production' ? 'combined' : 'dev'));

// Health check
app.get('/health', (req, res) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

// API routes
app.use('/api/v1', routes);

// Error handling (must be last)
app.use(errorHandler);

module.exports = app;

RESTful API-ontwerp

Ontwerp uw API-eindpunten volgens REST-conventies. Gebruik zelfstandige naamwoorden voor bronnen, HTTP-methoden voor acties en consistente antwoordformaten.

// src/routes/products.routes.js
const router = require('express').Router();
const { authenticate, authorize } = require('../middleware/auth');
const { validate } = require('../middleware/validator');
const { createProductSchema, updateProductSchema } = require('../validators/product');
const productController = require('../controllers/product.controller');

router.get('/', productController.getAll);
router.get('/:id', productController.getById);
router.post('/',
  authenticate,
  authorize('admin'),
  validate(createProductSchema),
  productController.create
);
router.put('/:id',
  authenticate,
  authorize('admin'),
  validate(updateProductSchema),
  productController.update
);
router.delete('/:id',
  authenticate,
  authorize('admin'),
  productController.delete
);

module.exports = router;

Controllers moeten dun zijn en bedrijfslogica delegeren aan serviceklassen:

// src/controllers/product.controller.js
const productService = require('../services/product.service');
const { asyncHandler } = require('../utils/asyncHandler');

exports.getAll = asyncHandler(async (req, res) => {
  const { page = 1, limit = 20, sort = 'created_at', order = 'DESC', search } = req.query;

  const result = await productService.findAll({
    page: parseInt(page),
    limit: Math.min(parseInt(limit), 100),
    sort,
    order,
    search,
  });

  res.json({
    success: true,
    data: result.products,
    pagination: {
      page: result.page,
      limit: result.limit,
      total: result.total,
      totalPages: result.totalPages,
    },
  });
});

exports.create = asyncHandler(async (req, res) => {
  const product = await productService.create(req.body);
  res.status(201).json({
    success: true,
    data: product,
  });
});

MySQL Verbindingspooling met mysql2

MySQL Architectuur van verbindingspoolToepassingGelijktijdige verzoekenVerzoekthread 1Verzoek discussie 2Verzoek discussie 3Verzoek discussie 4...Verzoek Thread NVerbindingspool (mysql2)ActiefInactiefVerbinding 1 — ActiefVerbinding 2 — ActiefVerbinding 3 — ActiefVerbinding 4 — InactiefVerbinding 5 — Inactief-verbindingLimiet: 10 | waitForConnections: waarenableKeepAlive: waar | wachtrijLimiet: 0MySQLDatabaseserverGegevensopslagIndexenInnoDB-motorPool hergebruikt verbindingen, waardoor de overhead van het maken van nieuwe verbindingen per verzoek wordt vermeden

Het poolen van verbindingen is van cruciaal belang voor de prestaties. Hetmysql2-pakket biedt een op Promise gebaseerde API met kant-en-klare instructies en pooling van verbindingen.

// src/config/database.js
const mysql = require('mysql2/promise');
const logger = require('./logger');

const pool = mysql.createPool({
  host: process.env.DB_HOST || 'localhost',
  port: parseInt(process.env.DB_PORT) || 3306,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
  waitForConnections: true,
  connectionLimit: parseInt(process.env.DB_POOL_SIZE) || 10,
  queueLimit: 0,
  enableKeepAlive: true,
  keepAliveInitialDelay: 30000,
  timezone: '+00:00',
  typeCast: function (field, next) {
    if (field.type === 'TINY' && field.length === 1) {
      return field.string() === '1';
    }
    return next();
  },
});

// Test connection on startup
pool.getConnection()
  .then(conn => {
    logger.info('MySQL connected successfully');
    conn.release();
  })
  .catch(err => {
    logger.error('MySQL connection failed:', err.message);
    process.exit(1);
  });

module.exports = pool;

Gebruik altijd geparametriseerde query's om SQL-injectie te voorkomen:

// NEVER do this
const query = `SELECT * FROM users WHERE email = '${email}'`;

// ALWAYS use parameterized queries
const [rows] = await pool.execute(
  'SELECT id, email, first_name, last_name FROM users WHERE email = ?',
  [email]
);

Sequelize ORM

Voor toepassingen met complexe gegevensrelaties biedt Sequelize een volledig functionele ORM met modeldefinities, associaties, migraties en het bouwen van query's.

// src/models/Product.js
const { DataTypes } = require('sequelize');
const sequelize = require('../config/sequelize');

const Product = sequelize.define('Product', {
  id: {
    type: DataTypes.UUID,
    defaultValue: DataTypes.UUIDV4,
    primaryKey: true,
  },
  name: {
    type: DataTypes.STRING(255),
    allowNull: false,
    validate: {
      notEmpty: true,
      len: [2, 255],
    },
  },
  description: {
    type: DataTypes.TEXT,
    allowNull: true,
  },
  price: {
    type: DataTypes.DECIMAL(10, 2),
    allowNull: false,
    validate: {
      min: 0,
    },
  },
  sku: {
    type: DataTypes.STRING(100),
    unique: true,
    allowNull: false,
  },
  stock_quantity: {
    type: DataTypes.INTEGER,
    defaultValue: 0,
    validate: {
      min: 0,
    },
  },
  is_active: {
    type: DataTypes.BOOLEAN,
    defaultValue: true,
  },
}, {
  tableName: 'products',
  timestamps: true,
  underscored: true,
  paranoid: true, // Soft deletes
  indexes: [
    { fields: ['sku'], unique: true },
    { fields: ['is_active'] },
    { fields: ['price'] },
    { fields: ['created_at'] },
  ],
});

// Associations
Product.associate = (models) => {
  Product.belongsTo(models.Category, { foreignKey: 'category_id' });
  Product.hasMany(models.OrderItem, { foreignKey: 'product_id' });
  Product.belongsToMany(models.Tag, { through: 'product_tags' });
};

module.exports = Product;

Authenticatie met JWT

Implementeer staatloze authenticatie met behulp van JSON-webtokens. Gebruik toegangstokens voor API-aanvragen en vernieuwtokens voor sessiebeheer.

// src/services/auth.service.js
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const { User } = require('../models');
const ApiError = require('../utils/ApiError');

const SALT_ROUNDS = 12;
const ACCESS_TOKEN_EXPIRY = '15m';
const REFRESH_TOKEN_EXPIRY = '7d';

exports.register = async ({ email, password, firstName, lastName }) => {
  const existingUser = await User.findOne({ where: { email } });
  if (existingUser) {
    throw new ApiError(409, 'Email already registered');
  }

  const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
  const user = await User.create({
    email,
    password: hashedPassword,
    first_name: firstName,
    last_name: lastName,
  });

  const tokens = generateTokens(user);
  return { user: sanitizeUser(user), ...tokens };
};

exports.login = async ({ email, password }) => {
  const user = await User.findOne({ where: { email } });
  if (!user || !(await bcrypt.compare(password, user.password))) {
    throw new ApiError(401, 'Invalid email or password');
  }

  const tokens = generateTokens(user);
  return { user: sanitizeUser(user), ...tokens };
};

function generateTokens(user) {
  const accessToken = jwt.sign(
    { userId: user.id, email: user.email, role: user.role },
    process.env.JWT_SECRET,
    { expiresIn: ACCESS_TOKEN_EXPIRY }
  );

  const refreshToken = jwt.sign(
    { userId: user.id, tokenType: 'refresh' },
    process.env.JWT_REFRESH_SECRET,
    { expiresIn: REFRESH_TOKEN_EXPIRY }
  );

  return { accessToken, refreshToken };
}

function sanitizeUser(user) {
  const { password, ...userData } = user.toJSON();
  return userData;
}

De authenticatie-middleware verifieert tokens op beschermde routes:

// src/middleware/auth.js
const jwt = require('jsonwebtoken');
const ApiError = require('../utils/ApiError');

exports.authenticate = (req, res, next) => {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    throw new ApiError(401, 'Access token required');
  }

  const token = authHeader.split(' ')[1];
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      throw new ApiError(401, 'Access token expired');
    }
    throw new ApiError(401, 'Invalid access token');
  }
};

exports.authorize = (...roles) => {
  return (req, res, next) => {
    if (!roles.includes(req.user.role)) {
      throw new ApiError(403, 'Insufficient permissions');
    }
    next();
  };
};

Invoervalidatie met Joi

Valideer alle inkomende gegevens voordat deze uw bedrijfslogica bereiken. Joi biedt een krachtige schemagebaseerde validatiebibliotheek.

// src/validators/product.js
const Joi = require('joi');

exports.createProductSchema = Joi.object({
  name: Joi.string().min(2).max(255).required(),
  description: Joi.string().max(5000).optional(),
  price: Joi.number().positive().precision(2).required(),
  sku: Joi.string().alphanum().max(100).required(),
  stock_quantity: Joi.number().integer().min(0).default(0),
  category_id: Joi.string().uuid().required(),
  tags: Joi.array().items(Joi.string().uuid()).optional(),
  is_active: Joi.boolean().default(true),
});

exports.updateProductSchema = Joi.object({
  name: Joi.string().min(2).max(255),
  description: Joi.string().max(5000).allow(null),
  price: Joi.number().positive().precision(2),
  stock_quantity: Joi.number().integer().min(0),
  category_id: Joi.string().uuid(),
  is_active: Joi.boolean(),
}).min(1);

// src/middleware/validator.js
exports.validate = (schema) => {
  return (req, res, next) => {
    const { error, value } = schema.validate(req.body, {
      abortEarly: false,
      stripUnknown: true,
    });

    if (error) {
      const errors = error.details.map(detail => ({
        field: detail.path.join('.'),
        message: detail.message,
      }));
      return res.status(400).json({
        success: false,
        message: 'Validation failed',
        errors,
      });
    }

    req.body = value;
    next();
  };
};

Middleware voor foutafhandeling

Gecentraliseerde foutafhandeling zorgt voor consistente foutreacties en voorkomt dat gevoelige informatie naar clients lekt.

// src/utils/ApiError.js
class ApiError extends Error {
  constructor(statusCode, message, errors = []) {
    super(message);
    this.statusCode = statusCode;
    this.errors = errors;
    this.isOperational = true;
    Error.captureStackTrace(this, this.constructor);
  }
}

module.exports = ApiError;

// src/utils/asyncHandler.js
exports.asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

// src/middleware/errorHandler.js
const logger = require('../config/logger');

exports.errorHandler = (err, req, res, next) => {
  let statusCode = err.statusCode || 500;
  let message = err.message || 'Internal Server Error';

  // Sequelize validation errors
  if (err.name === 'SequelizeValidationError') {
    statusCode = 400;
    message = 'Validation error';
  }

  // Sequelize unique constraint
  if (err.name === 'SequelizeUniqueConstraintError') {
    statusCode = 409;
    message = 'Resource already exists';
  }

  // Log server errors
  if (statusCode >= 500) {
    logger.error({
      message: err.message,
      stack: err.stack,
      url: req.originalUrl,
      method: req.method,
      ip: req.ip,
    });
  }

  res.status(statusCode).json({
    success: false,
    message,
    ...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
    ...(err.errors?.length && { errors: err.errors }),
  });
};

Queryoptimalisatie en indexering

Efficiënte databasequery's zijn cruciaal voor backend-prestaties. Volg deze strategieën om uw MySQL-query's snel te houden.

  • Gebruik indexen voor vaak opgevraagde kolommen- Voeg indexen toe voor kolommen die worden gebruikt in WHERE-clausules, JOIN-voorwaarden en ORDER BY-instructies. Samengestelde indexen moeten de meest linkse prefixregel volgen.
  • Vermijd SELECT *- Geef altijd de kolommen op die u nodig heeft. Hierdoor wordt de gegevensoverdracht verminderd en kan MySQL dekkingsindexen gebruiken.
  • Gebruik EXPLAIN om queries te analyseren- VoerEXPLAINuit vóór uw queries om het uitvoeringsplan te begrijpen. Zoek naar volledige tabelscans, bestandssorteerbewerkingen en tijdelijke tabellen.
  • Optimaliseer paginering- Gebruik voor grote datasets cursorgebaseerde paginering (keyset paginering) in plaats van OFFSET, wat langzaam wordt bij hoge paginanummers.
// Inefficient OFFSET pagination
const [rows] = await pool.execute(
  'SELECT * FROM products ORDER BY created_at DESC LIMIT ? OFFSET ?',
  [limit, (page - 1) * limit]
);

// Efficient cursor-based pagination
const [rows] = await pool.execute(
  `SELECT id, name, price, created_at FROM products
   WHERE created_at < ?
   ORDER BY created_at DESC
   LIMIT ?`,
  [cursor, limit]
);

Databasemigraties

Wijzig productiedatabases nooit handmatig. Gebruik Sequelize-migraties voor versiegestuurde schemawijzigingen.

// migrations/20250101000000-create-products-table.js
module.exports = {
  up: async (queryInterface, Sequelize) => {
    await queryInterface.createTable('products', {
      id: {
        type: Sequelize.UUID,
        defaultValue: Sequelize.UUIDV4,
        primaryKey: true,
      },
      name: {
        type: Sequelize.STRING(255),
        allowNull: false,
      },
      price: {
        type: Sequelize.DECIMAL(10, 2),
        allowNull: false,
      },
      sku: {
        type: Sequelize.STRING(100),
        unique: true,
        allowNull: false,
      },
      category_id: {
        type: Sequelize.UUID,
        references: {
          model: 'categories',
          key: 'id',
        },
        onUpdate: 'CASCADE',
        onDelete: 'SET NULL',
      },
      created_at: {
        type: Sequelize.DATE,
        defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
      },
      updated_at: {
        type: Sequelize.DATE,
        defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'),
      },
    });

    await queryInterface.addIndex('products', ['sku']);
    await queryInterface.addIndex('products', ['category_id']);
    await queryInterface.addIndex('products', ['created_at']);
  },

  down: async (queryInterface) => {
    await queryInterface.dropTable('products');
  },
};

Snelheidslimiet

Bescherm uw API tegen misbruik met snelheidslimiet. Gebruikexpress-rate-limitmet een Redis-winkel voor gedistribueerde implementaties.

// src/middleware/rateLimiter.js
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const redis = require('../config/redis');

exports.apiLimiter = rateLimit({
  store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }),
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,
  message: {
    success: false,
    message: 'Too many requests, please try again later',
  },
  standardHeaders: true,
  legacyHeaders: false,
});

exports.authLimiter = rateLimit({
  store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }),
  windowMs: 15 * 60 * 1000,
  max: 5,
  message: {
    success: false,
    message: 'Too many login attempts, please try again later',
  },
  skipSuccessfulRequests: true,
});

Logging met Winston

Productietoepassingen hebben gestructureerde loggen nodig met meerdere transporten en logniveaus.

// src/config/logger.js
const winston = require('winston');

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  defaultMeta: { service: 'api-server' },
  transports: [
    new winston.transports.File({
      filename: 'logs/error.log',
      level: 'error',
      maxsize: 5242880, // 5MB
      maxFiles: 5,
    }),
    new winston.transports.File({
      filename: 'logs/combined.log',
      maxsize: 5242880,
      maxFiles: 10,
    }),
  ],
});

if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.combine(
      winston.format.colorize(),
      winston.format.simple()
    ),
  }));
}

module.exports = logger;

Docker Implementatie

Containeriseer uw applicatie voor consistente implementaties in verschillende omgevingen.

# Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:20-alpine
WORKDIR /app
RUN addgroup -g 1001 -S appgroup && \
    adduser -S appuser -u 1001 -G appgroup
COPY --from=builder /app/node_modules ./node_modules
COPY src/ ./src/
COPY migrations/ ./migrations/
COPY package.json ./
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "src/server.js"]
# docker-compose.yml
version: '3.8'
services:
  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DB_HOST=mysql
      - DB_USER=app_user
      - DB_PASSWORD_FILE=/run/secrets/db_password
      - DB_NAME=myapp
    depends_on:
      mysql:
        condition: service_healthy
    restart: unless-stopped

  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
      MYSQL_DATABASE: myapp
      MYSQL_USER: app_user
      MYSQL_PASSWORD_FILE: /run/secrets/db_password
    volumes:
      - mysql_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  mysql_data:

Conclusie

Het bouwen van een robuuste Node.js- en MySQL-backend vereist aandacht voor architectuur, beveiliging, prestaties en operationele aspecten. Door een strakke projectstructuur op te zetten, de juiste authenticatie en validatie te implementeren, databasequery's te optimaliseren en uw implementatie in containers te plaatsen, creëert u een backend die veilig, performant en onderhoudbaar is. Begin met de basisbeginselen die in deze handleiding worden beschreven, meet de prestaties van uw toepassing onder realistische belasting en herhaal de knelpunten die u ontdekt. De hier gepresenteerde patronen zijn bewezen in duizenden productietoepassingen en zullen dienen als een solide basis voor uw backendsystemen.