Mini-chat met Node.js + SQLite

Van frontend naar database in kleine, begrijpelijke stappen

Pad Architectuur → Frontend → Backend → Database → Veiligheid

Architectuur overzicht

Browser (HTML + JS)
fetch()
Node.js server (http module)
sqlite3
SQLite database

Frontend en backend communiceren via HTTP. Server beheert de database.

Setup


mkdir chat-app && cd chat-app
npm init -y
npm install sqlite3
mkdir public
  
Bestandsstructuur:
server.mjs → backend code (.mjs = ES modules)
public/index.html → frontend

Frontend: HTML structuur


<button id="laadBerichten">Laad berichten</button>

<form id="verstuurContainer">
  <label for="verstuur_input">Bericht:</label>
  <input id="verstuur_input" type="text">
  <button type="submit">Verstuur</button>
</form>

<div id="berichtenContainer"></div>
    

Frontend: Berichten tonen


const toonBerichten = (data) => {
  const container = document.getElementById('berichtenContainer')
  container.innerHTML = ''
  
  data.forEach(bericht => {
    const div = document.createElement('div')
    div.classList.add('bericht')
    
    const naam = document.createElement('p')
    naam.textContent = bericht.name
    
    const tekst = document.createElement('p')
    tekst.textContent = bericht.message
    
    div.appendChild(naam)
    div.appendChild(tekst)
    container.appendChild(div)
  })
}
    

✓ Gebruikt textContent voor veiligheid (geen XSS)

Frontend: Berichten ophalen


const fetchBerichten = () => {
  fetch('/messages')
    .then(res => res.json())
    .then(data => toonBerichten(data))
}

// Koppel aan knop
document.getElementById('laadBerichten')
  .addEventListener('click', fetchBerichten)
    

Frontend: Bericht versturen


document.getElementById('verstuurContainer')
  .addEventListener('submit', (e) => {
    e.preventDefault()
    
    const input = document.getElementById('verstuur_input')
    const tekst = input.value.trim()
    if (!tekst) return
    
    fetch('/messages', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ 
        message: tekst, 
        user_id: 1 
      })
    })
    .then(() => {
      input.value = ''
      fetchBerichten()
    })
  })
    

Frontend: CSS styling


.bericht {
  display: flex;
  gap: 1rem;
  padding: 0.5rem;
  border-bottom: 1px solid #eee;
}

.bericht p:first-child {
  font-weight: bold;
  min-width: 100px;
}
    

Backend: Setup


import sqlite3 from 'sqlite3'
import fs from 'fs'
import path from 'path'
import http from 'http'

const __dirname = import.meta.dirname // Node 22+

const db = new sqlite3.Database('chat.db', (err) => {
  if (err) {
    console.error('Database error:', err)
    return
  }
  console.log('Database connected')
  createTables()
})
    
Oudere Node? Gebruik path.dirname(fileURLToPath(import.meta.url))

Backend: Tabellen aanmaken


const createTables = () => {
  db.exec(`
    CREATE TABLE IF NOT EXISTS users (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      name TEXT NOT NULL,
      age INTEGER
    );
    
    CREATE TABLE IF NOT EXISTS messages (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      user_id INTEGER NOT NULL,
      message TEXT NOT NULL,
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      FOREIGN KEY (user_id) REFERENCES users(id)
    );
  `, (err) => {
    if (err) console.error(err)
    else insertUser('Anton', 25) // Demo user
  })
}
    

Backend: Data invoegen


const insertUser = (name, age) => {
  const stmt = db.prepare('INSERT INTO users (name, age) VALUES (?, ?)')
  stmt.run(name, age)
  stmt.finalize()
}

const insertMessage = (user_id, message) => {
  const stmt = db.prepare(
    'INSERT INTO messages (user_id, message) VALUES (?, ?)'
  )
  stmt.run(user_id, message, (err) => {
    if (err) console.error('Insert error:', err)
  })
  stmt.finalize()
}
    
Prepared statements beschermen tegen SQL injection

Backend: Berichten ophalen (JOIN)


const getAllMessages = (res) => {
  const query = `
    SELECT 
      messages.id as msg_id,
      users.name,
      messages.message,
      messages.created_at
    FROM messages
    INNER JOIN users ON messages.user_id = users.id
    ORDER BY messages.created_at DESC
  `
  
  db.all(query, (err, rows) => {
    if (err) {
      res.statusCode = 500
      return res.end(JSON.stringify({ error: 'Database error' }))
    }
    res.setHeader('Content-Type', 'application/json')
    res.end(JSON.stringify(rows))
  })
}
    

Backend: HTTP server


const server = http.createServer((req, res) => {
  // Serveer HTML
  if (req.method === 'GET' && req.url === '/') {
    const htmlPath = path.join(__dirname, 'public', 'index.html')
    fs.readFile(htmlPath, 'utf8', (err, data) => {
      if (err) {
        res.statusCode = 500
        return res.end('Error loading page')
      }
      res.setHeader('Content-Type', 'text/html')
      res.end(data)
    })
  }
  // ... routes volgen
})
    

Backend: Routes (vervolg)


  // GET messages
  else if (req.method === 'GET' && req.url === '/messages') {
    getAllMessages(res)
  }
  
  // POST message
  else if (req.method === 'POST' && req.url === '/messages') {
    let body = ''
    req.on('data', chunk => body += chunk)
    req.on('end', () => {
      try {
        const { user_id, message } = JSON.parse(body)
        if (!message?.trim()) {
          res.statusCode = 400
          return res.end(JSON.stringify({ error: 'Message required' }))
        }
        insertMessage(user_id, message)
        res.statusCode = 201
        res.end(JSON.stringify({ success: true }))
      } catch (e) {
        res.statusCode = 400
        res.end(JSON.stringify({ error: 'Invalid JSON' }))
      }
    })
  }
    

Backend: Server starten


  // 404 handler
  else {
    res.statusCode = 404
    res.end('Not found')
  }
})

const PORT = 3000
server.listen(PORT, () => {
  console.log(`Server running at http://localhost:${PORT}`)
})