Skip to content

Frontend design

De lib/ directory bevat de core business logic en utilities van de VEMAP frontend applicatie. Het is gestructureerd volgens een modulaire architectuur met strikte naleving van software design principes.

Voor de volledige file tree, klik hier.

  • Directorylib/
    • app.factory.js Alpine.js app initialization
    • Directoryalpine/ Alpine.js data components
      • Directoryauth/ Authenticatie forms
      • Directorycharts/ Chart data components
        • Directoryadmin/ Admin dashboards
        • Directorymanager/ Manager dashboards
        • Directoryorganisation/ Organisatie dashboards
        • Directoryshared/ Herbruikbare chart types
      • Directoryforms/ Form handling components
      • Directorymail/ Email editor
      • Directorystats/ Statistics components
        • Directoryadmin/
        • Directorymanager/
        • Directoryorganisation/
      • Directoryviews/ View data components
    • Directoryconfig/ Applicatie configuratie
    • Directorymiddleware/ Astro middleware
    • Directoryparser/ Data parsers
    • Directoryservices/ Business logic services
    • Directorystores/ Alpine.js reactive stores
    • Directoryutils/ Utility functies
      • Directoryhttp/ HTTP client en headers
      • Directorycharts/ Chart utilities

De lib architectuur is gebouwd op drie fundamentele software design principes:

Elke module heeft één duidelijk doel en is zo eenvoudig mogelijk gehouden:

  • Utility functies zijn standalone en onafhankelijk
  • Services encapsuleren complexe business logic in simpele interfaces
  • Stores bieden simpele state management zonder boilerplate

Voorbeeld:

// Utils zijn simpel en doen één ding goed
export const validateEmail = (email) => {
return /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(email);
};
// Services bieden simpele interfaces voor complexe operaties
export const userLogOut = async () => {
await post(createFullUrl("authentication/logout"), authHeaders());
deleteCookie("user_session");
deleteRefreshToken();
window.location.href = "/login";
};

Herbruikbare logica is gecentreerd in utility functies en services:

  • Utils voorkomen code duplicatie voor validatie, formatting, HTTP calls
  • Stores centraliseren state management
  • Services delen API communicatie logica
  • Config centraliseert alle configuratie

Voorbeeld:

// Zonder DRY: Error handling overal verschillend
try {
const response = await fetch(url);
if (!response.ok) alert("Error!");
} catch (e) {
console.error(e);
}
// Met DRY: Gecentreerde error handling
import { handleHttpError } from "./utils/errors";
const result = handleHttpError(response, "create", {
notificationStore: Alpine.store("notificationStore"),
});

Elke module categorie heeft een specifieke verantwoordelijkheid:

CategorieVerantwoordelijkheidVoorbeeld
ServicesBusiness logic & API communicatieauth.service.js
UtilsHerbruikbare utility functiesvalidation.js, dates.js
StoresState management (Alpine.js)user.store.js
AlpineUI logic & form handlinglogin.data.js
ConfigConfiguratie & environment settingsapp.js, environments.js
MiddlewareRequest interceptie & auth checksauth.middleware.js
ParserData parsing & formattingpapaparse.init.js

Data Flow (SoC in actie):

Alpine Component → Service → HTTP Client → API
↓ ↓ ↓
UI Logic Business Logic Transport

Stores en services gebruiken dependency injection voor flexibiliteit en testbaarheid. Deze injectie is noodzakelijk als gevolg van Astro.js middleware met SSR, waarbij Alpine.js geïnitialiseerd moet worden voordat de pagina volledig gebouwd is.

De dependency injection wordt geconfigureerd via astro.config.mjs:

astro.config.mjs
import { defineConfig } from "astro/config";
import alpinejs from "@astrojs/alpinejs";
export default defineConfig({
output: "server",
adapter: netlify({
edgeMiddleware: true,
}),
integrations: [
alpinejs({
entrypoint: "./src/lib/app.factory.js",
}),
react(),
],
});

De app.factory.js fungeert als entry point voor Alpine.js initialisatie:

// app.factory.js registreert alle stores met dependencies
Alpine.store("userStore", userStore(Alpine.reactive.persist));
Alpine.store("slideoverStore", slideoverStore(Alpine.effect));
Alpine.store("taskStore", taskStore(Alpine.reactive.persist, Alpine.nextTick));
  1. Server-side rendering: Astro.js bouwt de pagina op de server
  2. Middleware execution: Alpine.js middleware wordt uitgevoerd
  3. Dependency injection: Stores worden geïnjecteerd met Alpine.js dependencies
  4. Client hydration: Alpine.js wordt geactiveerd op de client
  5. Reactive updates: Stores zijn klaar voor interactiviteit

Dit zorgt ervoor dat Alpine.js correct functioneert in de SSR omgeving zonder hydration mismatches.

ModuleBeschrijvingDocumentatie
auth.serviceAWS Cognito authenticatieAuth Service
fileUpload.serviceS3 file uploadsFile Upload Service
ModuleBeschrijvingDocumentatie
validationInput validatieValidation Utils
cookiesCookie managementCookie Utils
tokensJWT utilitiesToken Utils
datesDatum utilitiesDate Utils
stringsString utilitiesString Utils
formattersData formattersFormatters
functionsAlgemene utilitiesFunction Utils
errorsError handlingError Handling
sessionSession utilitiesSession Utils
http/clientHTTP requestsHTTP Client
http/headersHTTP headersHTTP Headers
authAuth utilitiesAuth Utils
constantsApp constantsConstants
filesFile utilitiesFile Utils
ModuleBeschrijvingDocumentatie
user.storeGebruikersbeheerUser Store
project.storeProjectbeheerProject Store
closure.storeAfsluitingenbeheerClosure Store
notification.storeToast notificationsNotification Store
slideover.storeSlideover formsSlideover Store
formModal.storeForm modalsForm Modal Store
tenant.storeTenant beheerTenant Store
files.storeBestanden beheerFiles Store
task.storeStakeholder takenTask Store
event.storeEvent timelineEvent Store
metrics.storeApplicatie metricsMetrics Store

Deze best practices illustreren hoe KISS, DRY en SoC in de praktijk worden toegepast.

Services voor API Calls - Verberg complexiteit achter simpele interfaces:

Direct HTTP calls met veel boilerplate:

const response = await fetch("/api/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${getCookie("token")}`,
"x-user-id": user.id,
},
body: JSON.stringify(data),
});
if (!response.ok) {
// Error handling...
}

Via service layer (simpel en duidelijk):

import { createUser } from "../services/user.service";
const user = await createUser(userData);

Utils voor Herbruikbare Logic - Schrijf logica één keer, gebruik het overal:

Dezelfde validatie overal herhalen:

// In component A
const isValid = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/.test(email);
// In component B
const isValid = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/.test(email);
// In component C
const isValid = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/.test(email);

Via validation util (één implementatie):

import { validateEmail } from "../utils/validation";
const isValid = validateEmail(email); // Overal hetzelfde gedrag

Stores voor Shared State - Eén bron van waarheid:

Component-local state (duplicatie):

// Component A
let users = [];
fetch("/api/users").then((r) => (users = r.data));
// Component B
let users = []; // Dezelfde data opnieuw ophalen!
fetch("/api/users").then((r) => (users = r.data));

Via store (gedeelde state):

// Eén keer ophalen, overal beschikbaar
await Alpine.store("userStore").getUsers();
const users = Alpine.store("userStore").users;

Error Handling via Utils - Scheidt error handling van business logic:

Error handling mixed met business logic:

async function saveUser(data) {
try {
const response = await fetch("/api/users", {
method: "POST",
body: JSON.stringify(data),
});
if (response.status === 400) {
alert("Ongeldige data!");
} else if (response.status === 403) {
alert("Geen toegang!");
} else if (response.status === 500) {
alert("Server error!");
}
// Business logic mixed met error handling
} catch (e) {
console.error(e);
}
}

Via error handler (gescheiden concerns):

// Business logic
async function saveUser(data) {
const response = await post(createFullUrl("users"), apiHeaders(), data);
if (!response || response.error) {
// Error handling is gedelegeerd
handleHttpError(response, "create", {
notificationStore: Alpine.store("notificationStore"),
customTitle: "Gebruiker Opslaan Mislukt",
});
return;
}
// Alleen business logic hier
Alpine.store("userStore").addUsers([response.data]);
}

Flexibiliteit & Testbaarheid - Inject dependencies in plaats van hard-coding:

Hard-coded dependencies:

export const myStore = {
data: [],
save() {
sessionStorage.setItem("data", JSON.stringify(this.data));
},
};

Met dependency injection:

export default function myStore(persist) {
return {
data: persist([]).using(sessionStorage),
// Nu testbaar met mock storage
};
}

Voorkom typos - Gebruik constants in plaats van magic strings:

Magic strings (foutgevoelig):

if (response.status === 404) {
// Typo risico
}
if (user.role === "ROLE_ADMIN") {
// Typo risico
}

Via constants (type-safe):

import { STATUS_CODES, ROLES } from "../utils/constants";
if (response.status === STATUS_CODES.NOT_FOUND) {
// Type-safe
}
if (user.role === ROLES.ADMIN) {
// Type-safe
}