Home

Backend Template

File structure

project-root
├── package.json           # Project metadata, dependencies, scripts, etc.
├── .env                   # Environment variables (local setup)
├── .env.sample            # Sample environment variables file for reference
├── server.js              # Main server entry point

├── config
   └── dbConnect.js       # Database connection setup

├── routes
   └── userRouter.js      # Defines user-related routes

├── controllers
   └── userController.js  # Handles user-related logic (controller for user actions)

├── models
   └── User.js            # User model/schema (database structure and methods)

├── middleware
   ├── validateRequest.js # Middleware for request validation
   ├── jwt.js             # JWT authentication middleware
   └── roleCheck.js       # Role-based access control middleware

└── validation
    └── userValidation.js  # User-specific validation rules

Server

Server.js

import express from "express";
import cors from "cors";
import cookieParser from "cookie-parser";
import connectDB from "./config/dbConnect.js";
import userRouter from "./routes/userRouter.js";

connectDB(); // start db connection

const PORT = process.env.PORT;

const app = express();

app.use(cookieParser());
app.use(cors({credentials:true, origin:"http://localhost:5173"}));
app.use(express.json()); // body parser

app.use("/users", userRouter);
// ... more routes when needed

app.listen(PORT, () => {
  console.log(`Server is listening on port: ${PORT}`)
});

Environment

.env

PORT=3000
MONGODB_URI=mongodb://127.0.0.1:27017
DATABASE=users

Database Connection

config/dbConnect.js

import mongoose from "mongoose";

mongoose.connection.on('error', (error) => {
    console.log('DB error after initial connection:', error);
});

const connectDB = async () => {
    try {
        await mongoose.connect(process.env.MONGO_URL, {
            dbName: process.env.DATABASE
        });
        console.log('Connected to MongoDB!');
    } catch (error) {
        console.error('Connection error:', error);
    }
};

export default connectDB;

Router

routes/userRouter.js

import {Router} from "express";
import * as user from "../controllers/userController.js"
import { authenticate } from "../middleware/jwt.js";
import { roleCheck } from "../middleware/roleCheck.js";

const userRouter = Router();

userRouter
    .post("/register", user.createUser)
    .post("/login", user.userLogin)
    .get("/", authenticate, user.dashboard)
    .put("/", authenticate, roleCheck(["editor", "admin"]), user.editMessage)
    .delete("/", authenticate, roleCheck(["admin"]), user.deleteUser)

export default userRouter;

Controller

controllers/userController.js

import { User } from "../models/User.js";
import { generateToken } from "../middleware/jwt.js";

export const createUser = async(req, res) => {
    try {
        const user = await User.create(req.body);
        res.status(201).json({msg: "Neuer Benutzer wurde gespeichert", user});
    } catch (error) {
        console.log(error);
        res.status(500).json({msg:"Server error!"})  
    }
}

export const userLogin = async(req, res)=>{
    try {
    const { email, password } = req.body;
      const user = await User.findOne({ email });

      if (!user) return res.status(404).json({msg:"Unbekannter Benutzer!"});

      const passwordMatch = await user.authenticate(password);

      if (!passwordMatch) return res.status(401).json({msg:"Fehlerhafte Anmeldedaten!"})
        
      const token = generateToken({userId:user._id});
      return res.status(200).cookie("jwt", token, {httpOnly:true, maxAge: 60*60*1000}).json({msg:"Anmeldung erfolgreich!"});
    } catch (error) {
    console.log(error);
    res.status(500).json({msg:"Server error!"})  
    };
}

export const dashboard = async(req, res) => {
    try {
        res.status(200).send(`Hallo ${req.user.name}, willkommen auf unserer Seite!`);
    } catch (error) {
        console.log(error);
        res.status(500).json({msg:"Server error!"})  
    }
}

Model

models/User.js

import {Schema, model} from "mongoose";
import bcrypt from "bcrypt";

const userSchema = new Schema({
    name:{type:String, required:true},
    email: { type: String, unique: true, required:true },
    password: { type: String, required:true, select:false },
    roles:{[
        type:String,
        enum: ["user", "editor", "admin"]
    ]}
});

/*
Function is called before a document is saved into the db
with User.create() or user.save()
*/
userSchema.pre("save", async function(next){
    try {
        if (this.isModified("password")) { // only hash if the password has been modified
          const hash = await bcrypt.hash(this.password, 12);
          this.password = hash;
        }
        next();
    } catch (error) {
        next(error);
    }
});

/*
Name after methods can be random, function is called from the
controller, by attaching it to the user instance (not User model):
user.authenticate()
*/
userSchema.methods.authenticate = async function(password){
    return await bcrypt.compare(password, this.password);
};

/*
toJSON is a special method, the function is called whenever somewhere
data is converted to json (by res.send or res.json)
*/
userSchema.methods.toJSON = function(){
    const user = this.toObject();
    delete user.password;
    return user;
};

export const User = new model('User', userSchema);

Authorization Middleware (jsonwebtoken)

middleware/jwt.js

import jwt from "jsonwebtoken";
import User from "../model/User.js";

export const generateToken = (payload) => {
    return jwt.sign(payload, process.env.JWT_SECRET, {
      expiresIn: process.env.JWT_EXPIRES_IN || "1h",
    });
  };

const verifyToken = (token) => {
    return jwt.verify(token, process.env.JWT_SECRET);
};
  
export const authenticate = async (req, res, next) => {
    try {
        const authHeader = req.headers['authorization'];
        const token = authHeader && authHeader.split(' ').at(-1);
        // If token is saved in a cookie
        // const token = req.cookies.jwt;

        if (!token) {
            return res.status(401).json({ msg: "Authentification failed!" });
        }

        const decoded = verifyToken(token);
        
        const user = await User.findById(decoded.userId);
        
        if (!user) {
            return res.status(404).json({ msg: "User not found!" });
        }
        
        req.user = user;
        next();
    } catch (error) {
        return res.status(403).json({ msg: "Authentification error!" });
    }
};

Validation Middleware

middleware/validateRequest.js

// Middleware to validate JSON against the provided schema
const validateRequest = (validateFn) => (req, res, next) => {
    const valid = validateFn(req.body); // Compare body with the compiled function
    if (!valid) {
       // If errors occured ,they are stored in the errors property of the function
      const errors = validateFn.errors.map((err) => err.message);
      return res.status(400).json({ errors });
    }
    next();
  };
  
  export default validateRequest;

Role Check Middleware

middleware/roleCheck.js

export const roleCheck = (targetRole) => {

    // This is a common pattern in Express middleware
    // It's a function that returns another function
    // aka. a "higher-order function"

    // The inner function is the actual middleware
    // It has access to the targetRole variable

    return function roleCheckMiddleware(req, res, next) {
        const hasRights = targetRole.includes(req.user.role)
        // for more than one role (if req.user.roles is an array)
        // const hasRights = targetRoles.some(role => req.user.roles.includes(role));
        console.log(req.user.role, {hasRights});
        
        if (!hasRights) {
            return res.status(403).json({ error: "Nicht autorisiert!" })
        }
        next()
    }
}

Validation Schema

validation/userValidation.js

import Ajv from "ajv";
import addFormats from "ajv-formats";
import addErrors from "ajv-errors";

// Initialize ajv
const ajv = new Ajv({ allErrors: true }); // Show all errors, don't stop after the first one
addFormats(ajv); // Add format validators (e.g., email, URL)
addErrors(ajv); // Add support for custom error messages

const userSchema = {
  type: "object",
  properties: {
    name: {
      type: "string",
      minLength: 1,
      errorMessage: "Bitte einen Namen eingeben!",
    },
    email: {
      type: "string",
      format: "email",
      errorMessage: "Bitte eine gültige E-Mail eingeben!",
    },
    password: {
      type: "string",
      minLength: 8,
      maxLength: 30,
      pattern: "(?=.*\\d)(?=.*[a-z])(?=.*[A-Z])",
      errorMessage: {
        minLength: "Das Passwort hat nicht die erforderliche Länge (8-30 Zeichen)!",
        maxLength: "Das Passwort hat nicht die erforderliche Länge (8-30 Zeichen)!",
        pattern:
          "Das Passwort muss aus Groß-und Kleinbuchstaben bestehen und mindestens eine Zahl enthalten!",
      },
    },
    website: {
      type: ["string", "null"],
      pattern: "^$|^(https?://.+)$",  // Allow empty string or a valid URL
      // format: "uri",
      nullable: true, // Allows undefined or null
      errorMessage: {
        "type": "Die Webseite muss eine Zeichenkette oder null sein!",
        "pattern": "Die Webseite muss eine gültige URL enthalten oder leer sein!"
      },
    },
    location: {
      type: "string",
      enum: ["Deutschland", "Österreich", "Schweiz"],
      errorMessage: "Anmeldung nur in der DACH-Region möglich!",
    },
    blogposts: {
      type: "array",
      items: {
        type: "string",
        pattern: "^[0-9a-fA-F]{24}$", // ObjectId validation
        errorMessage: "Ungültige ObjectId!",
      },
    },
  },
  required: ["name", "email", "password"],
  additionalProperties: false,
  errorMessage: {
    required: {
      name: "Bitte einen Namen eingeben!",
      email: "Bitte eine gültige E-Mail eingeben!",
      password: "Das Passwort ist erforderlich!",
    },
  },
};

// Compile the schema and return true or false (if errors occur)
const validateUser = ajv.compile(userSchema); 

export default validateUser;

Seeding

seed.js

import { faker } from "@faker-js/faker";
import Participant from "./models/Participant.js";
import Breakoutroom from "./models/Breakoutroom.js";
import connectDB from "./config/dbConnect.js";

connectDB(); // start db connection

const deleteAllRooms = async() => {
    console.log("starting to delete");
    return await Participant.deleteMany();
};

const deleteAllParticipants = async() => {
    return await Breakoutroom.deleteMany();
};

const participants = [];
let counter = 1; // To get all participants into rooms

const createParticipant = async() => {
    const newParticipant = new Participant({
        name: faker.name.fullName(),
        city: faker.address.city(),
        email: faker.internet.email(),
    });

    const result = await newParticipant.save();
    participants.push(result["_id"]);
};

const createRoom = async(num) => {
    const newRoom = new Breakoutroom({
        number: num,
        participants: getRandomIds(),
    });

    const result = await newRoom.save();
};

const createParticipants = async (count = 1) => {
    for (let i = 0; i < count; i++) {
        await createParticipant();
    }
};

const createRooms = async (count) => {
    for (let i = 0; i < count; i++) {
        await createRoom(i + 1);
    }
};

const getRandomIds = () => {
    if (participants.length < 1) return [];
    
    let max = participants.length;
    if (participants.length > 3) max = Math.abs(max/2);
    
    let number = Math.floor(Math.random()*(max - 1 + 1) + 1);
    const selection = [];
    
    if (counter === +process.argv[3]) number = participants.length;
    
    for (let i = 0; i < number; i++) {
        selection.push(participants[i]);
    }
    
    counter++;
    participants.splice(0, number);
    return selection;
};

try {
    if (!process.argv.includes("doNotDelete")) {
        console.log("Deleting all documents...");
        await deleteAllRooms();
        await deleteAllParticipants();
        console.log("finished deleting!");
    }

    const count1 = process.argv[2] === "doNotDelete" ? undefined : process.argv[2];
    const count2 = process.argv[3] === "doNotDelete" ? undefined : process.argv[3];

    await createParticipants(count1);
    console.log("Participants created!");
    await createRooms(count2);
    console.log("Rooms created!");

    process.exit(0);
} catch (error) {
    console.error(error);
    process.exit(1);
}