44 KiB
User Management Rendszer - Feladat Leírás
Projekt Áttekintés
Egy User Management (felhasználó kezelési) rendszer elkészítése Node.js és Express.js használatával, Clean Architecture elveket követve, CQRS (Command Query Responsibility Segregation) pattern alkalmazásával.
Célkitűzés
Egy RESTful API létrehozása felhasználók kezeléséhez (CRUD műveletek), amely követi a modern szoftver architektúra elveket és jól strukturált, karbantartható kódot eredményez.
Technológiai Stack
Kötelező Technológiák
- Node.js (v18 vagy újabb)
- Express.js (v5.2.1 vagy újabb) - Web framework
- ES6 Modules -
import/exportszintaxis használata - JSON File Storage - Adattárolás fájl alapú megoldással (későbbi adatbázis integrációhoz előkészítve)
Fejlesztői Eszközök
- nodemon - Automatikus újraindítás fejlesztés közben
Architektúra Áttekintés
A projekt 4 fő réteget tartalmaz:
src/
├── API/ # Prezentációs réteg (Web API)
├── Application/ # Üzleti logika réteg (CQRS)
├── Domain/ # Domain modell és interfészek
└── Infrastructure/ # Adatelérési réteg és külső szolgáltatások
Rétegek Részletes Leírása
1. Domain Réteg (Domain Layer)
- Felelősség: Az alkalmazás magját képező interfészek és domain modellek
- Tartalma: Repository interfészek (IUserRepository)
- Függőség: Nincs más rétegre való függősége
2. Infrastructure Réteg (Infrastructure Layer)
- Felelősség: Technikai implementációk (adatbázis, fájlkezelés, külső API-k)
- Tartalma: Repository implementációk, adattárolás
- Függőség: Domain rétegtől függ (implementálja az interfészeket)
3. Application Réteg (Application Layer)
- Felelősség: Üzleti logika, use case-ek implementálása
- Tartalma: Commands, CommandHandlers, Queries, QueryHandlers
- Függőség: Domain rétegtől függ
- CQRS Pattern: Írás (Command) és olvasás (Query) műveletek szétválasztása
4. API Réteg (API Layer)
- Felelősség: HTTP kérések kezelése, routing, válaszok küldése
- Tartalma: Controllers, Routers, Server konfiguráció
- Függőség: Application és Infrastructure rétegtől függ
CQRS Pattern Magyarázat
Mi az a CQRS?
Command Query Responsibility Segregation - Az írási és olvasási műveletek szétválasztása.
Command (Parancs) - Írási műveletek
- Megváltoztatja a rendszer állapotát
- Nem ad vissza értéket (vagy csak a létrehozott entitást)
- Példák: CreateUser, UpdateUser, DeleteUser
Struktúra:
- Command osztály - Tartalmazza a parancs adatait
- CommandHandler osztály - Végrehajtja a parancsot, validálja az adatokat
Query (Lekérdezés) - Olvasási műveletek
- Nem változtatja meg a rendszer állapotát
- Adatokat ad vissza
- Példák: GetAllUsers, GetUserById
Struktúra:
- Query osztály - Tartalmazza a lekérdezés paramétereit
- QueryHandler osztály - Végrehajtja a lekérdezést
Részletes Implementációs Útmutató
1. Projekt Inicializálás
1.1 Projekt struktúra létrehozása
Backend/
└── elso gyakorlat/
├── package.json
└── src/
├── API/
│ ├── server.js
│ ├── controllers/
│ │ └── userController.js
│ └── routers/
│ └── userRouter.js
├── Application/
│ └── users/
│ ├── command/
│ │ ├── createUserCommand.js
│ │ ├── createUserCommandHandler.js
│ │ ├── updateUserCommand.js
│ │ ├── updateUserCommandHandler.js
│ │ ├── deleteUserCommand.js
│ │ └── deleteUserCommandHandler.js
│ └── query/
│ ├── getAllUsersQuery.js
│ ├── getAllUsersQueryHandler.js
│ ├── getUserByIdQuery.js
│ └── getUserByIdQueryHandler.js
├── Domain/
│ └── IUserRepository.js
└── Infrastructure/
├── user.json
└── userRepository.js
1.2 package.json létrehozása
Cél: Node.js projekt konfigurációs fájl létrehozása ES6 modul támogatással.
Részletes leírás:
A package.json az npm projekt szíve - tartalmazza a metaadatokat, függőségeket és script-eket.
Kulcsfontosságú mezők:
1. Alapinformációk:
name: Projekt neve (lowercase, kötőjellel)version: Verzió szám (semantic versioning: MAJOR.MINOR.PATCH)description: Rövid leírásmain: Belépési pont (itt nem használt)
2. "type": "module" ⚠️ KRITIKUS!
- Engedélyezi az ES6 modulok használatát (
import/export) - Nélküle csak
require()/module.exportsműködne (CommonJS) - Ez kötelező, hogy
importszintaxist használhassunk!
3. Scripts:
start: A szerver indítása nodemon-nalnodemon src/API/server.js- automatikus újraindítás fájl módosításkor- Így nem kell minden változtatás után manuálisan újraindítani
test: Placeholder teszt script (később Jest/Mocha tesztek jöhetnek ide)
4. Dependencies (Futásidejű függőségek):
express: ^5.2.1 - Web framework^(caret): Automatikus minor/patch update-ek engedélyezése- 5.x.x verzió tartományon belül
nodemon: ^3.1.11 - Fejlesztői szerver automatikus újraindítása- Érdemes
devDependencies-be tenni (csak fejlesztéshez kell)
- Érdemes
Létrehozási módok:
Manuális: Hozd létre a fájlt és másold be a tartalmat.
npm init:
npm init -y # Gyors, alapértelmezett értékekkel
Utána add hozzá: "type": "module"
Későbbi bővítések:
"devDependencies": {
"nodemon": "^3.1.11", // Ide kellene
"jest": "^29.0.0" // Teszteléshez
},
"engines": {
"node": ">=18.0.0" // Node verzió megkötés
}
📄 Teljes package.json tartalom
{
"name": "elso-gyakorlat",
"version": "1.0.0",
"description": "User Management System",
"main": "index.js",
"type": "module",
"scripts": {
"start": "nodemon src/API/server.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"express": "^5.2.1",
"nodemon": "^3.1.11"
}
}
FONTOS: Ne felejtsed el hozzáadni a "type": "module" sort!
1.3 Függőségek telepítése
npm install
2. Domain Réteg Implementálás
2.1 IUserRepository.js - Repository Interface
Fájl: src/Domain/IUserRepository.js
Cél: Egy interfész-szerű osztály létrehozása, amely definiálja a user repository alapvető műveleteit.
Részletes leírás:
A Domain réteg középpontjában az interfészek állnak. JavaScript-ben nincs natív interfész támogatás (mint pl. TypeScript-ben vagy Java-ban), de osztályokkal szimulálhatjuk ezt a viselkedést.
Mit kell létrehozni:
-
IUserRepository osztály - Ez lesz a "szerződés", amit minden repository implementációnak követnie kell
-
5 metódus, mind aszinkron (
async):getAll()- Összes felhasználó lekérése (paraméter nélkül)getById(id)- Egy felhasználó lekérése ID alapjáncreate(user)- Új felhasználó létrehozása (user objektumot vár paraméterként)update(id, userData)- Felhasználó módosítása (ID és új adatok)delete(id)- Felhasználó törlése (ID alapján)
-
Error dobás - Minden metódus dobjon hibát alapértelmezetten (
throw new Error('Method not implemented'))- Miért? Mert ez biztosítja, hogy ha valaki implementálja ezt az osztályt, de elfelejt egy metódust felülírni, egyből hibát kapjon
Fontos elvek:
- Ez az osztály NEM tartalmaz implementációt, csak az "interfészt" definiálja
- Minden metódus async, mert később fájlkezelés/adatbázis műveleteket fogunk végezni
- Ez a Dependency Inversion Principle része - a magasabb szintű kód ettől az interface-től függ, nem a konkrét implementációtól
📄 Teljes kód megtekintése
export class IUserRepository {
async getAll() {
throw new Error('Method not implemented');
}
async getById(id) {
throw new Error('Method not implemented');
}
async create(user) {
throw new Error('Method not implemented');
}
async update(id, userData) {
throw new Error('Method not implemented');
}
async delete(id) {
throw new Error('Method not implemented');
}
}
3. Infrastructure Réteg Implementálás
3.1 user.json - Adat fájl
Fájl: src/Infrastructure/user.json
Cél: Egyszerű JSON fájl alapú adattárolás létrehozása.
Részletes leírás:
Ez lesz az "adatbázisunk" - egy egyszerű JSON fájl, amely a felhasználókat tárolja. Később ezt könnyedén lecserélhetjük valódi adatbázisra (MongoDB, PostgreSQL, stb.) anélkül, hogy az alkalmazás többi részét módosítanánk.
Mit tartalmaz:
- Kezdetben egy üres tömb (
[]) - A repository fog írni/olvasni ebből a fájlból
- Minden user objektumként lesz tárolva a tömbben
Miért JSON fájl?
- Egyszerű és gyors kezdéshez
- Nem kell adatbázist telepíteni
- Könnyen olvasható és debuggolható
- Demonstrálja a repository pattern használatát
📄 Fájl tartalma megtekintése
[]
3.2 userRepository.js - Repository Implementáció
Fájl: src/Infrastructure/userRepository.js
Cél: Az IUserRepository interfész konkrét implementációja JSON fájl alapú tárolással.
Részletes leírás:
Ez az osztály valósítja meg a tényleges adatelérési logikát. Kiterjeszti (extends) az IUserRepository interfészt, tehát felül kell írnia minden metódust.
Importok és inicializálás:
- fs/promises - Node.js fájlrendszer modul aszinkron verzió (promise-based)
- path - Fájl útvonalak kezeléséhez
- fileURLToPath - ES6 modulokban szükséges a
__dirnameeléréséhez - IUserRepository - A szülő interfész
Fontos ES6 Modules trükk:
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
ES6 modulokban nem elérhető a __dirname, így ezt a workaround-ot kell használni.
Metódusok részletes működése:
1. getAll() - Összes user lekérése
fs.readFile()- Beolvassa a JSON fájlt (async)JSON.parse()- String-ből objektummá alakítja- Try-catch: ha a fájl nem létezik/üres, üres tömböt ad vissza
- Fontos: Ez a metódus lesz a többi metódus alapja is!
2. getById(id) - Egy user keresése
- Meghívja a
getAll()-t find()- Tömb metódus, megkeresi az első egyező elemet- Ha nincs találat,
undefined-et ad vissza
3. create(user) - Új user létrehozása
- Lekéri az összes user-t
- Új objektum létrehozása:
id: Date.now().toString()- Timestamp alapú egyedi ID...user- Spread operator, bemásol minden property-tcreatedAt- ISO formátumú timestamp
users.push()- Hozzáadja a tömb végéhezfs.writeFile()- Visszamenti a fájlbaJSON.stringify(users, null, 2)- Pretty print, 2 szóköz indentálás
- Visszaadja az új user objektumot
4. update(id, userData) - User módosítása
findIndex()- Megkeresi az user indexét a tömbben- Ha
index === -1, nem létezik →nullvisszatérés - Merge művelet spread operator-ral:
...users[index]- Régi adatok...userData- Új adatok (felülírják a régieket)id: users[index].id- ID megtartása (biztonsági okokból)updatedAt- Új timestamp
- Fájl frissítése és módosított user visszaadása
5. delete(id) - User törlése
filter()- Új tömb létrehozása az adott user nélkül- Hossz összehasonlítás: ha egyenlő, nem volt mit törölni →
false - Fájl frissítése és
truevisszaadása
Best practices:
- Minden metódus
async- konzisztens API - Try-catch csak ahol szükséges (getAll)
- Spread operator használata immutábilis műveleteknél
- Pretty print a JSON fájlban - könnyebb debuggolni
📄 Teljes kód megtekintése
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import { IUserRepository } from '../Domain/IUserRepository.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const USER_FILE = path.join(__dirname, 'user.json');
export class UserRepository extends IUserRepository {
async getAll() {
try {
const data = await fs.readFile(USER_FILE, 'utf-8');
return JSON.parse(data);
} catch (error) {
return [];
}
}
async getById(id) {
const users = await this.getAll();
return users.find(user => user.id === id);
}
async create(user) {
const users = await this.getAll();
const newUser = {
id: Date.now().toString(),
...user,
createdAt: new Date().toISOString()
};
users.push(newUser);
await fs.writeFile(USER_FILE, JSON.stringify(users, null, 2));
return newUser;
}
async update(id, userData) {
const users = await this.getAll();
const index = users.findIndex(user => user.id === id);
if (index === -1) return null;
users[index] = {
...users[index],
...userData,
id: users[index].id,
updatedAt: new Date().toISOString()
};
await fs.writeFile(USER_FILE, JSON.stringify(users, null, 2));
return users[index];
}
async delete(id) {
const users = await this.getAll();
const filteredUsers = users.filter(user => user.id !== id);
if (users.length === filteredUsers.length) return false;
await fs.writeFile(USER_FILE, JSON.stringify(filteredUsers, null, 2));
return true;
}
}
4. Application Réteg - Query Implementálás
4.1 GetAllUsersQuery
Fájl: src/Application/users/query/getAllUsersQuery.js
Cél: Query objektum létrehozása az összes felhasználó lekérdezéséhez.
Részletes leírás:
A CQRS pattern-ben minden lekérdezésnek (Query) van egy saját Query osztálya, még akkor is, ha az üres. Ez a konzisztencia és a kód későbbi bővíthetősége szempontjából fontos.
Miért üres ez az osztály?
- Az összes user lekérdezéséhez nincs szükség paraméterekre (nincs szűrés, nincs ID, stb.)
- A konstruktor üres marad
- Később könnyedén bővíthető (pl. pagination:
page,limitparaméterek)
CQRS elv: Minden use case-nek (ebben az esetben: "add meg az összes user-t") saját Query objektuma van, még ha paraméter nélküli is.
📄 Teljes kód megtekintése
export class GetAllUsersQuery {
constructor() {}
}
4.2 GetAllUsersQueryHandler
Fájl: src/Application/users/query/getAllUsersQueryHandler.js
Cél: A GetAllUsersQuery feldolgozása és végrehajtása.
Részletes leírás:
A Handler az, amely ténylegesen végrehajtja a műveletet. Ez a "use case" implementáció.
Struktúra:
-
Konstruktor:
- Fogad egy
userRepositoryparamétert (Dependency Injection) - Eltárolja azt instance változóban (
this.userRepository) - Miért DI? Később könnyű tesztelni (mock repository-val), és nem függ konkrét implementációtól
- Fogad egy
-
handle(query) metódus:
- Fogadja a Query objektumot (jelen esetben üres)
- Meghívja a repository
getAll()metódusát - Visszaadja az eredményt
async/awaithasználata az aszinkron művelethez
Ez a Handler felelőssége:
- Query feldolgozása
- Repository meghívása
- Eredmény visszaadása
- (Itt lehetne üzleti logika, validáció, stb. - de ennél az egyszerű lekérdezésnél nincs rá szükség)
Nem a Handler felelőssége:
- HTTP kérések kezelése (ezt a Controller végzi)
- Adatbázis műveletek (ezt a Repository végzi)
📄 Teljes kód megtekintése
export class GetAllUsersQueryHandler {
constructor(userRepository) {
this.userRepository = userRepository;
}
async handle(query) {
return await this.userRepository.getAll();
}
}
4.3 GetUserByIdQuery
Fájl: src/Application/users/query/getUserByIdQuery.js
Cél: Query objektum egyedi felhasználó lekérdezéséhez ID alapján.
Részletes leírás:
Ez a Query már tartalmaz paramétert - a keresendő user ID-t.
Konstruktor paraméter:
id- A keresendő felhasználó egyedi azonosítója (string formátumban)- Eltároljuk
this.id-ben, hogy a Handler hozzáférjen
Példa használat:
const query = new GetUserByIdQuery('1707825600000');
CQRS elv: A Query objektum tiszta adatstruktúra (Data Transfer Object - DTO), csak az adatokat hordozza, logika nélkül.
📄 Teljes kód megtekintése
export class GetUserByIdQuery {
constructor(id) {
this.id = id;
}
}
4.4 GetUserByIdQueryHandler
Fájl: src/Application/users/query/getUserByIdQueryHandler.js
Cél: Egyedi felhasználó lekérdezésének feldolgozása validációval.
Részletes leírás:
Konstruktor:
- Ugyanaz a DI pattern, mint a GetAllUsersQueryHandler-nél
handle(query) metódus lépései:
-
Repository meghívása:
const user = await this.userRepository.getById(query.id);- Lekéri a user-t a query-ben kapott ID alapján
- A repository
undefined-et ad vissza, ha nincs találat
-
Validáció:
if (!user) { throw new Error('User not found'); }- Ha nincs találat, kivételt dob
- Ez az üzleti logika része: egy használhatatlan Query-t hibának tekintünk
-
Eredmény visszaadása:
- Ha megvan a user, visszaadjuk
Error Handling:
- A dobott hiba később a Controller-ben lesz elkapva
- A Controller alakítja át HTTP 404 válasszá
- Ez szépen szétválasztja a rétegeket: Application réteg nem tud HTTP-ről, csak üzleti hibákat dob
📄 Teljes kód megtekintése
export class GetUserByIdQueryHandler {
constructor(userRepository) {
this.userRepository = userRepository;
}
async handle(query) {
const user = await this.userRepository.getById(query.id);
if (!user) {
throw new Error('User not found');
}
return user;
}
}
5. Application Réteg - Command Implementálás
5.1 CreateUserCommand
Fájl: src/Application/users/command/createUserCommand.js
Cél: Command objektum új felhasználó létrehozásához.
Részletes leírás:
CQRS: Command vs Query
- A Query adatokat kér le (olvasás) - nem változtatja a rendszer állapotát
- A Command műveletet hajt végre (írás) - megváltoztatja a rendszer állapotát
Konstruktor paraméterek:
name(string) - A felhasználó neve (kötelező)email(string) - A felhasználó email címe (kötelező)age(number) - A felhasználó életkora (opcionális)
Fontos:
- Az ID-t NEM adjuk meg - azt a Repository generálja automatikusan
- A
createdAttimestamp-et sem - azt is a Repository adja hozzá - A Command csak az "üzleti adatokat" tartalmazza
Példa használat:
const command = new CreateUserCommand('John Doe', 'john@example.com', 30);
Clean Architecture elv: A Command egy egyszerű DTO (Data Transfer Object), tisztán adatok, logika nélkül.
📄 Teljes kód megtekintése
export class CreateUserCommand {
constructor(name, email, age) {
this.name = name;
this.email = email;
this.age = age;
}
}
5.2 CreateUserCommandHandler
Fájl: src/Application/users/command/createUserCommandHandler.js
Cél: Új felhasználó létrehozási parancs végrehajtása validációval.
Részletes leírás:
Konstruktor:
- DI pattern: userRepository injektálás
handle(command) metódus lépései:
1. Validáció (Üzleti szabályok):
if (!command.name || !command.email) {
throw new Error('Name and email are required');
}
- Name és email kötelező mezők
- Ez üzleti logika - az Application réteg felelőssége
- Ha valamelyik hiányzik, kivételt dobunk
- Later: Bővíthető email formátum ellenőrzéssel, egyediség ellenőrzéssel, stb.
2. User objektum összeállítása:
const userData = {
name: command.name,
email: command.email,
age: command.age
};
- A Command adataiból létrehozunk egy plain objektumot
- Csak a "tiszta" adatok mennek tovább a Repository-nak
- Az age lehet
undefinedha nem adták meg - ez rendben van
3. Repository meghívása:
return await this.userRepository.create(userData);
- A Repository hozzáadja az ID-t és a createdAt-et
- Visszaadja a teljes user objektumot (ID-val és timestamp-el együtt)
CommandHandler felelősségei:
- ✅ Validáció (üzleti szabályok)
- ✅ Adatok előkészítése
- ✅ Repository meghívása
- ❌ NEM HTTP kezelés (azt a Controller végzi)
- ❌ NEM adatbázis műveletek (azt a Repository végzi)
📄 Teljes kód megtekintése
export class CreateUserCommandHandler {
constructor(userRepository) {
this.userRepository = userRepository;
}
async handle(command) {
if (!command.name || !command.email) {
throw new Error('Name and email are required');
}
const userData = {
name: command.name,
email: command.email,
age: command.age
};
return await this.userRepository.create(userData);
}
}
5.3 UpdateUserCommand
Fájl: src/Application/users/command/updateUserCommand.js
Cél: Command objektum felhasználó módosításához.
Részletes leírás:
Konstruktor paraméterek:
id(string) - A módosítandó user egyedi azonosítója (kötelező!)name(string) - Az új/frissített névemail(string) - Az új/frissített emailage(number) - Az új/frissített életkor
Különbség a CreateUserCommand-hoz képest:
- Ez tartalmaz ID-t is - tudnunk kell, melyik user-t módosítjuk
- Az ID a URL-ből jön majd (pl.
PUT /users/123456)
Példa használat:
const command = new UpdateUserCommand(
'1707825600000',
'John Updated',
'john.new@example.com',
31
);
📄 Teljes kód megtekintése
export class UpdateUserCommand {
constructor(id, name, email, age) {
this.id = id;
this.name = name;
this.email = email;
this.age = age;
}
}
5.4 UpdateUserCommandHandler
Fájl: src/Application/users/command/updateUserCommandHandler.js
Cél: Felhasználó módosítási parancs végrehajtása.
Részletes leírás:
handle(command) metódus lépései:
1. Adatok előkészítése:
const userData = {
name: command.name,
email: command.email,
age: command.age
};
- Összegyjük a módosítandó adatokat
- Fontos: Az ID-t NEM küldjük tovább a userData-ban!
- Az ID külön paraméterként megy a repository-nak (biztonság és tisztaság)
2. Repository meghívása:
const updatedUser = await this.userRepository.update(command.id, userData);
- A repository
update()metódusa várja az ID-t és az új adatokat - Visszaad
null-t ha nem található a user - Visszaadja a frissített user objektumot (updatedAt timestamp-el) ha sikeres
3. Validáció (létezés ellenőrzés):
if (!updatedUser) {
throw new Error('User not found');
}
- Ha
nulljött vissza, nem létezett ilyen ID-jű user - Kivételt dobunk, amit a Controller HTTP 404-re alakít
4. Eredmény visszaadása:
- A frissített user objektum megy vissza a Controller-nek
Későbbi bővítési lehetőségek:
- Validáció csak a megváltozott mezőkre (partial update)
- Verzió kontroll (optimistic locking)
- Jogosultság ellenőrzés (csak saját profil módosítható)
📄 Teljes kód megtekintése
export class UpdateUserCommandHandler {
constructor(userRepository) {
this.userRepository = userRepository;
}
async handle(command) {
const userData = {
name: command.name,
email: command.email,
age: command.age
};
const updatedUser = await this.userRepository.update(command.id, userData);
if (!updatedUser) {
throw new Error('User not found');
}
return updatedUser;
}
}
5.5 DeleteUserCommand
Fájl: src/Application/users/command/deleteUserCommand.js
Cél: Command objektum felhasználó törléséhez.
Részletes leírás:
Konstruktor paraméterek:
id(string) - A törlendő user egyedi azonosítója
Egyszerű struktúra:
- Csak egy ID-t tartalmaz - ez az egyetlen információ, ami kell a törléshez
- Hasonló a GetUserByIdQuery-hez, de szándékban különbözik (írás vs olvasás)
Példa használat:
const command = new DeleteUserCommand('1707825600000');
CQRS szétválasztás: Bár hasonló a Query-hez, külön osztály kell, mert ez Command (írási művelet, állapot változtatás).
📄 Teljes kód megtekintése
export class DeleteUserCommand {
constructor(id) {
this.id = id;
}
}
5.6 DeleteUserCommandHandler
Fájl: src/Application/users/command/deleteUserCommandHandler.js
Cél: Felhasználó törlési parancs végrehajtása.
Részletes leírás:
handle(command) metódus lépései:
1. Repository meghívása:
const deleted = await this.userRepository.delete(command.id);
- A repository
delete()metódusa boolean-t ad vissza true- sikeres törlésfalse- nem létezett ilyen ID-jű user
2. Validáció:
if (!deleted) {
throw new Error('User not found');
}
- Ha törlés sikertelen (false), kivételt dobunk
- Ez konzisztens a többi handler validációjával
3. Visszatérési érték:
- Nincs explicit return!
- DELETE művelet nem ad vissza adatot (ún. "void" művelet)
- A Controller HTTP 204 No Content választ küld
Fontos különbség:
- Ez az egyetlen Handler, ami nem ad vissza user objektumot
- REST convention: DELETE művelet után nincs response body
Későbbi bővítési lehetőségek:
- Soft delete (csak megjelölés töröltként, fizikai törlés helyett)
- Cascade delete (kapcsolódó adatok törlése is)
- Audit log (ki, mikor törölte)
📄 Teljes kód megtekintése
export class DeleteUserCommandHandler {
constructor(userRepository) {
this.userRepository = userRepository;
}
async handle(command) {
const deleted = await this.userRepository.delete(command.id);
if (!deleted) {
throw new Error('User not found');
}
}
}
6. API Réteg Implementálás
6.1 userController.js - Controller
Fájl: src/API/controllers/userController.js
Cél: HTTP kérések kezelése és koordináció az Application réteggel.
Részletes leírás:
A Controller a Presentation réteg része - ő az, aki tudja, mi az a HTTP, mi az a request és response. Az Application réteget (Handler-ek) használja az üzleti logika végrehajtására.
Fájl szerkezete:
1. Importok:
- Repository és összes Query/Command osztály
- Összes QueryHandler és CommandHandler osztály
- Összesen 11 import (1 Repository + 5 Query/Command pár)
2. Dependency Injection beállítása:
const userRepository = new UserRepository();
const getAllUsersQueryHandler = new GetAllUsersQueryHandler(userRepository);
// ... stb
- Egy közös repository példány minden handler-nek
- Handler példányok modul szinten (singleton pattern)
- Ez egyszerűsíti a kódot - nem kell minden metódusban létrehozni
3. Controller metódusok (5 db):
getAll(req, res) - Összes user lekérése
- Query létrehozása (paraméter nélkül)
- Handler meghívása
- Eredmény visszaküldése JSON formátumban
- Error handling: 500 (szerver hiba)
getById(req, res) - Egy user lekérése
- Query létrehozása
req.params.id-val (URL paraméter) - Handler meghívása
- Eredmény visszaküldése
- Error handling: 404 (nem található)
create(req, res) - Új user létrehozása
- Command létrehozása
req.bodyadatokból (POST body) - Handler meghívása
- Eredmény visszaküldése 201 Created státusszal
- Error handling: 400 (validációs hiba)
update(req, res) - User módosítása
- Command létrehozása ID-val (URL) + adatok (POST body)
- Handler meghívása
- Eredmény visszaküldése
- Error handling: 404 (nem található)
delete(req, res) - User törlése
- Command létrehozása csak ID-val
- Handler meghívása
- 204 No Content státusz, üres body
- Error handling: 404 (nem található)
HTTP Státusz kódok:
- 200 OK - Sikeres GET, PUT (default Express-ben)
- 201 Created - Sikeres POST (új resource)
- 204 No Content - Sikeres DELETE (nincs response body)
- 400 Bad Request - Validációs hiba (hiányzó/helytelen adatok)
- 404 Not Found - Resource nem található
- 500 Internal Server Error - Váratlan szerver hiba
Felelősségek:
- ✅ HTTP kérés/válasz kezelése
- ✅ URL paraméterek és body adatok kinyerése
- ✅ Command/Query objektumok létrehozása
- ✅ Handler-ek hívása
- ✅ HTTP státusz kódok beállítása
- ✅ Error handling és átkonvertálása HTTP válaszokra
- ❌ NINCS üzleti logika (azt a Handler végzi)
- ❌ NINCS adatbázis művelet (azt a Repository végzi)
📄 Teljes kód megtekintése
import { UserRepository } from '../../Infrastructure/userRepository.js';
import { GetAllUsersQuery } from '../../Application/users/query/getAllUsersQuery.js';
import { GetAllUsersQueryHandler } from '../../Application/users/query/getAllUsersQueryHandler.js';
import { GetUserByIdQuery } from '../../Application/users/query/getUserByIdQuery.js';
import { GetUserByIdQueryHandler } from '../../Application/users/query/getUserByIdQueryHandler.js';
import { CreateUserCommand } from '../../Application/users/command/createUserCommand.js';
import { CreateUserCommandHandler } from '../../Application/users/command/createUserCommandHandler.js';
import { UpdateUserCommand } from '../../Application/users/command/updateUserCommand.js';
import { UpdateUserCommandHandler } from '../../Application/users/command/updateUserCommandHandler.js';
import { DeleteUserCommand } from '../../Application/users/command/deleteUserCommand.js';
import { DeleteUserCommandHandler } from '../../Application/users/command/deleteUserCommandHandler.js';
const userRepository = new UserRepository();
const getAllUsersQueryHandler = new GetAllUsersQueryHandler(userRepository);
const getUserByIdQueryHandler = new GetUserByIdQueryHandler(userRepository);
const createUserCommandHandler = new CreateUserCommandHandler(userRepository);
const updateUserCommandHandler = new UpdateUserCommandHandler(userRepository);
const deleteUserCommandHandler = new DeleteUserCommandHandler(userRepository);
export class UserController {
async getAll(req, res) {
try {
const query = new GetAllUsersQuery();
const users = await getAllUsersQueryHandler.handle(query);
res.json(users);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
async getById(req, res) {
try {
const query = new GetUserByIdQuery(req.params.id);
const user = await getUserByIdQueryHandler.handle(query);
res.json(user);
} catch (error) {
res.status(404).json({ error: error.message });
}
}
async create(req, res) {
try {
const command = new CreateUserCommand(
req.body.name,
req.body.email,
req.body.age
);
const user = await createUserCommandHandler.handle(command);
res.status(201).json(user);
} catch (error) {
res.status(400).json({ error: error.message });
}
}
async update(req, res) {
try {
const command = new UpdateUserCommand(
req.params.id,
req.body.name,
req.body.email,
req.body.age
);
const user = await updateUserCommandHandler.handle(command);
res.json(user);
} catch (error) {
res.status(404).json({ error: error.message });
}
}
async delete(req, res) {
try {
const command = new DeleteUserCommand(req.params.id);
await deleteUserCommandHandler.handle(command);
res.status(204).send();
} catch (error) {
res.status(404).json({ error: error.message });
}
}
}
6.2 userRouter.js - Router
Fájl: src/API/routers/userRouter.js
Cél: RESTful route-ok definiálása és Controller metódusokhoz kötése.
Részletes leírás:
A Router az Express moduláris routing rendszere - külön fájlban definiálhatjuk a route-okat, majd a főalkalmazásba importálhatjuk.
Fájl szerkezete:
1. Importok:
express- Express frameworkUserController- A korábban létrehozott controller
2. Router és Controller példányosítás:
const router = express.Router();
const userController = new UserController();
3. Route definíciók (5 db):
REST API konvenciók:
| HTTP Metódus | URL Pattern | Controller Metódus | Leírás |
|---|---|---|---|
| GET | / |
getAll() |
Összes user listázása |
| GET | /:id |
getById() |
Egyedi user lekérése |
| POST | / |
create() |
Új user létrehozása |
| PUT | /:id |
update() |
User teljes módosítása |
| DELETE | /:id |
delete() |
User törlése |
Cél: Express alkalmazás konfigurálása, middleware-ek és route-ok beállítása, szerver indítása.
Részletes leírás:
Ez az alkalmazás belépési pontja (entry point) - ez a fájl indul el, amikor npm start-ot futtatunk.
Fájl szerkezete:
1. Importok:
import express from 'express';
import userRouter from './routers/userRouter.js';
- Express framework
- User router (korábban definiált route-ok)
2. Express app instance:
const app = express();
- Létrehozza az Express alkalmazás objektumot
- Ezen fogunk middleware-eket és route-okat regisztrálni
3. Konfigurációs változók:
const PORT = 3000;
- Port szám, ahol a szerver hallgatózni fog
- Később környezeti változóból is jöhet:
process.env.PORT || 3000
4. Middleware-ek:
app.use(express.json());
- Kritikus fontosságú! Ez parse-olja a JSON body-t a POST/PUT kérésekben
- Nélküle a
req.bodyundefined lenne - Az Express beépített middleware-je (korábban külön
body-parserpackage volt) - Automatikusan parse-olja ha
Content-Type: application/json
5. Route regisztráció:
app.use('/users', userRouter);
- Base path:
/users - A userRouter-ben definiált relatív útvonalak eléje kerül ez a prefix
- Így a
router.get('/')valójábanGET /userslesz - Moduláris: könnyen hozzáadhatunk más router-eket (pl.
/posts,/comments)
Teljes útvonalak:
GET /users→ összes userGET /users/:id→ egy userPOST /users→ új userPUT /users/:id→ user módosításDELETE /users/:id→ user törlés
6. Szerver indítása:
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
app.listen()- Express metódus a szerver indításához- Callback function - lefut, amikor a szerver sikeresen elindult
- Console log - visszajelzés, hogy minden OK
Fontos koncepciók:
Middleware sorrend:
- A middleware-ek sorrendben futnak!
express.json()a route-ok ELŐTT kell legyen- Így a route handler-ek már parse-olt body-t kapnak
További bővítési lehetőségek:
app.use(cors()); // CORS engedélyezése
app.use(morgan('dev')); // HTTP logging
app.use(helmet()); // Biztonsági headers
app.use('/posts', postRouter); // További entitások
📄 Teljes kód megtekintése
import express from 'express';
import userRouter from './routers/userRouter.js';
const app = express();
const PORT = 3000;
// Middleware
app.use(express.json());
// Routes
app.use('/users', userRouter);
// Server
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
import express from 'express';
import { UserController } from '../controllers/userController.js';
const router = express.Router();
const userController = new UserController();
router.get('/', (req, res) => userController.getAll(req, res));
router.get('/:id', (req, res) => userController.getById(req, res));
router.post('/', (req, res) => userController.create(req, res));
router.put('/:id', (req, res) => userController.update(req, res));
router.delete('/:id', (req, res) => userController.delete(req, res));
export default router;
6.3 server.js - Szerver konfiguráció
Fájl: src/API/server.js
import express from 'express';
import userRouter from './routers/userRouter.js';
const app = express();
const PORT = 3000;
// Middleware
app.use(express.json());
// Routes
app.use('/users', userRouter);
// Server
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Futtatás és Tesztelés
Szerver Indítása
npm start
Ez elindítja a nodemon-t, ami automatikusan újraindítja a szervert fájl módosítások esetén.
API Endpoint-ok Tesztelése
1. Összes User Lekérése
Request:
GET http://localhost:3000/users
Válasz:
[]
2. Új User Létrehozása
Request:
POST http://localhost:3000/users
Content-Type: application/json
{
"name": "John Doe",
"email": "john@example.com",
"age": 30
}
Válasz:
{
"id": "1707825600000",
"name": "John Doe",
"email": "john@example.com",
"age": 30,
"createdAt": "2026-02-13T10:00:00.000Z"
}
3. User Lekérése ID alapján
Request:
GET http://localhost:3000/users/1707825600000
Válasz:
{
"id": "1707825600000",
"name": "John Doe",
"email": "john@example.com",
"age": 30,
"createdAt": "2026-02-13T10:00:00.000Z"
}
4. User Módosítása
Request:
PUT http://localhost:3000/users/1707825600000
Content-Type: application/json
{
"name": "John Updated",
"email": "john.updated@example.com",
"age": 31
}
Válasz:
{
"id": "1707825600000",
"name": "John Updated",
"email": "john.updated@example.com",
"age": 31,
"createdAt": "2026-02-13T10:00:00.000Z",
"updatedAt": "2026-02-13T10:05:00.000Z"
}
5. User Törlése
Request:
DELETE http://localhost:3000/users/1707825600000
Válasz:
204 No Content
Tesztelési Eszközök
- Postman: GUI alapú API tesztelő
- Thunder Client: VS Code extension
- cURL: Parancssori eszköz
- REST Client: VS Code extension (.http fájlokkal)
User Adatmodell
{
"id": "string", // Automatikusan generált (timestamp)
"name": "string", // Kötelező
"email": "string", // Kötelező
"age": number, // Opcionális
"createdAt": "ISO8601", // Automatikusan generált létrehozáskor
"updatedAt": "ISO8601" // Automatikusan generált módosításkor
}
Hibakezelés
Validációs Hibák
- 400 Bad Request: Hiányzó kötelező mezők (name, email)
- Üzenet:
"Name and email are required"
Not Found Hibák
- 404 Not Found: User nem található (GET, PUT, DELETE)
- Üzenet:
"User not found"
Szerver Hibák
- 500 Internal Server Error: Váratlan hiba (pl. fájl írási probléma)
- Üzenet: Részletes hibaüzenet
Best Practice-ek és Elvek
1. Separation of Concerns (SoC)
- Minden réteg csak a saját felelősségével foglalkozik
- Controller nem tartalmaz üzleti logikát
- Repository nem validál
2. Dependency Inversion Principle (DIP)
- A magasabb szintű modulok nem függenek az alacsonyabb szintűektől
- Mindketten az absztrakciótól (interface) függenek
UserController→IUserRepositoryinterface-től függ, nem a konkrét implementációtól
3. Single Responsibility Principle (SRP)
- Minden osztálynak egy felelőssége van
- Command/Query külön handler-ben
- Repository csak adatelérésért felelős
4. CQRS Benefits
- Egyszerűbb kód, könnyebb megérteni
- Optimalizálható külön az olvasás és írás
- Könnyebb tesztelni
5. Async/Await használata
- Minden I/O művelet (fájl olvasás/írás) aszinkron
- Try-catch az error handling-hez
Gyakori Problémák és Megoldások
1. ES6 Module hibák
Hiba: SyntaxError: Cannot use import statement outside a module
Megoldás: package.json-ben add hozzá: "type": "module"
2. __dirname not defined
Hiba: ReferenceError: __dirname is not defined in ES module scope
Megoldás:
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
3. Port already in use
Hiba: Error: listen EADDRINUSE: address already in use :::3000
Megoldás:
- Másik port használata
- Folyamat leállítása: Windows-on
netstat -ano | findstr :3000majdtaskkill /PID <PID> /F
4. JSON parse error
Hiba: JSON fájl sérült vagy üres
Megoldás: Try-catch blokk a repository-ban, üres tömb visszaadása hiba esetén
Ellenőrző Lista (Checklist)
- Projekt struktúra létrehozva
- package.json konfigurálva (
"type": "module") - Függőségek telepítve
- Domain réteg: IUserRepository létrehozva
- Infrastructure réteg: UserRepository és user.json létrehozva
- Application réteg: Összes Query és Command létrehozva
- Application réteg: Összes Handler létrehozva
- API réteg: UserController létrehozva
- API réteg: userRouter létrehozva
- API réteg: server.js létrehozva és konfigurálva
- Szerver elindul hiba nélkül
- GET /users működik
- POST /users működik és ment a JSON fájlba
- GET /users/:id működik
- PUT /users/:id működik
- DELETE /users/:id működik
- Hibakezelés működik mindenhol
- Kód tiszta, kommentek megfelelőek
Összegzés
Ez a User Management rendszer egy jól strukturált, modern Node.js alkalmazás, amely követi a Clean Architecture és CQRS elveket. A kód könnyen karbantartható, skálázható, és előkészített későbbi bővítésekre (pl. adatbázis integráció).
Főbb tanulságok:
- Rétegelt architektúra előnyei
- CQRS pattern alkalmazása
- Dependency Injection használata
- REST API best practice-ek
- Async/Await és hibakezelés
- ES6 modulok használata Node.js-ben
Fejlesztési idő becslés: 3-5 óra (tapasztalattól függően)
Nehézségi szint: Közép-haladó
Dátum: 2026. február 13.
Verzió: 1.0