Node and express: intermediate

Layered architecture

Layered architecture

common layers in web applications:

  • controller layer (HTTP)
  • service layer (business logic)
  • model layer (database connection)

Layered architecture

controller: receives HTTP request, calls a service function for processing, sends HTTP response

service: gets called by a controller; can call other services and models (multiple); can return data to the controller; contains the main logic (business logic)

model: provides database access to services

Layered architecture

often a route (HTTP query) corresponds directly to a model (database table)

e.g. in a shopping application:

/products?category=phones&max_price=500

corresponds to:

SELECT * FROM products WHERE category = 'phones' AND price <= 500;

Layered architecture

sometimes there can be a lot going on in the service layer

example: sending a new message via POST to /messages

messageService could use:

  • authService (to authenticate / authorize the sender)
  • userModel (to find the recipient)
  • spamDetectionService
  • messageModel (to store the message)
  • notificationService (to notify the recipient - e.g. via SMS / email)

Layered architecture

common layers in express applications:

  • routing layer: receives an HTTP request and forwards it to a controller; potentially applies middleware
  • controller layer: receives HTTP requests, calls services, sends HTTP responses
  • service layer: contains the main logic (business logic)
  • model layer: does database interactions (queries, mutations)

Layered architecture

can be simplified for smaller projects, e.g.:

  • combined routing / controller / service layer
  • model layer

Source code structure

typical source code structure:

  • server.js (main entry point)
  • db.js (database connection)
  • routes/
  • controllers/
  • services/
  • models/

Routing

Routing

We can restructure our routes definitions by using Router objects

Routing

having everything in one place:

app.get('/accounts', () => {
  // ...
});
app.get('/accounts/:id', () => {
  // ...
});
app.post('/accounts', () => {
  // ...
});

app.get('/transactions', () => {
  // ...
});
// ...

Routing

cleaner structure:

app.use('/accounts', accountsRouter);
app.use('/transactions', transactionsRouter);

Routing

definition of accountsRouter in a separate file (e.g. routes/accounts.route.js)

import { Router } from 'express';

export const accountsRouter = Router();

accountsRouter.get('/', () => {
  // ...
});
accountsRouter.get('/:id', () => {
  // ...
});

Authentication and authorization: theory

Authentication and authorization: theory

topics:

  • terminology
  • roles and permissions
  • session tokens
  • passwords and password hashes

Terminology

  • authentication: Is this request really coming from user xyz?
  • authorization: Is user xyz authorized to see / modify this data?

Roles and permissions

examples for roles and permissions on a news site:

  • guests can see the first 500 characters of each article
  • users can see all articles; users can see comments; users can create, edit and delete their own comments
  • moderators can create, update and delete articles; moderators can delete comments

Session tokens

typical auth flow with tokens:

  • client sends authentication data (e.g. username + password) to the server
  • server checks for correctness
  • server sends back a secret "token" for this session (as a string)
  • client can access pages / APIs using that token
  • token will usually become invalid after some specific time / if the user logs out

Session tokens

session tokens are sent from the client to a server in HTTP headers:

  • Authorization header (needs client-side JavaScript)
  • Cookie header

Session tokens

example on the client: logging in via username and password and getting the token:

const loginRes = await fetch('/api/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    username: 'foo',
    password: 'bar',
  }),
});
const loginData = await loginRes.json();
const token = loginData.token;

Session tokens

example on the client: accessing some data via the token:

const notesRes = await fetch('/api/notes', {
  headers: { Authorization: `Bearer ${token}` },
});
const notes = await notesRes.json();

Passwords and password hashes

  • bad practice: storing passwords directly in the database (in plain text)
  • better: storing password hashes
  • ideal / standard: storing password hashes that are "salted" and "peppered"

Passwords and password hashes

plain data:

name    | password
--------+--------------------
alice   | 123456
bob     | 123456
charlie | abc123

Passwords and password hashes

data with hashed passwords:

name    | password hash
--------+---------------------------------
Alice   | e10adc3949ba59abbe56e057f20f883e
Bob     | e10adc3949ba59abbe56e057f20f883e
Charlie | e99a18c428cb38d5f260853678922e03

Passwords and password hashes

hashing algorithms - sorted from most secure to not secure:

  • Argon2
  • scrypt
  • bcrypt
  • PBKDF2
  • MD5

Authentication and authorization in Express

Example: Login Handler

app.post('/api/login', async (req, res) => {
  const success = await authService.validateLogin(
    req.body.username,
    req.body.password
  );
  if (success) {
    const token = authService.createRandomToken();
    await authService.saveSession(req.body.username, token);
    res.json({ token: token });
  } else {
    res.status(401).send();
  }
});

Example: Data Handler

app.get('/api/notes/:id', async (req, res) => {
  const authHeader = req.header('Authorization');
  const token = authHeader.split(' ')[1];
  const session = await authService.getSession(token);
  if (!session) {
    // not authenticated
    res.status(401).send();
    return;
  }
  const note = await notesService.getNote(req.params.id);
  const role = await authService.getRole(session);
  if (role === 'user' && session.userId === note.userId) {
    res.json(note);
  } else if (role === 'admin') {
    res.json(note);
  } else {
    // not authorized
    res.status(403).send();
  }
});

Configuration via environment variables

Configuration via environment variables

credentials (e.g. for databases) and configuration should be supplied via environment variables

credentials should not be under version control

.env file

common way to supply configuration and credentials: store in a file named .env, load as environment variables via dotenv

make sure .env is not under version control (add it to the .gitignore file)

.env file

example .env file:

PORT=3000
NODE_ENV=production
DB_URL=mongodb+srv://...
AUTH0_DOMAIN=xxx.eu.auth0.com

loading in JavaScript:

import dotenv from 'dotenv';

dotenv.config();

const PORT = process.env.PORT;
const DB_URL = process.env.DB_URL;

NODE_ENV

Environment variable NODE_ENV: important when using e.g. express

in production environments, NODE_ENV=production should always be set - otherwise the user will be able to see JavaScript error messages in detail (with stack traces)

Hosting

Hosting

hosting providers with free options:

  • glitch.com
  • render.com
  • fly.io
  • ...