# 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/export` szintaxis 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 Layer** (Domain réteg) - **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 Layer** (Infrastruktúra réteg) - **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 layer-től függ (implementálja az interfészeket) #### 3. **Application Layer** (Alkalmazás réteg) - **Felelősség**: Üzleti logika, use case-ek implementálása - **Tartalma**: Commands, CommandHandlers, Queries, QueryHandlers - **Függőség**: Domain layer-től függ - **CQRS Pattern**: Írás (Command) és olvasás (Query) műveletek szétválasztása #### 4. **API Layer** (API réteg) - **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 layer-tő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:** 1. **Command osztály** - Tartalmazza a parancs adatait 2. **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:** 1. **Query osztály** - Tartalmazza a lekérdezés paramétereit 2. **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ás - `main`: 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.exports` működne (CommonJS) - Ez kötelező, hogy `import` szintaxist használhassunk! **3. Scripts:** - `start`: A szerver indítása nodemon-nal - `nodemon 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 szerverAutomatikus újraindítás - Érdemes `devDependencies`-be tenni (csak fejlesztéshez kell) **Létrehozási módok:** **Manuális:** Hozd létre a fájlt és másold be a tartalmat. **npm init:** ```bash npm init -y # Gyors, alapértelmezett értékekkel ``` Utána add hozzá: `"type": "module"` **Későbbi bővítések:** ```json "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 ```json { "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 ```bash npm install ``` --- ### 2. Domain Layer 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 Layer 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:** 1. **IUserRepository osztály** - Ez lesz a "szerződés", amit minden repository implementációnak követnie kell 2. **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án - `create(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) 3. **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 ```javascript 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 Layer 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 ```json [] ```
#### 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:** 1. **fs/promises** - Node.js fájlrendszer modul aszinkron verzió (promise-based) 2. **path** - Fájl útvonalak kezeléséhez 3. **fileURLToPath** - ES6 modulokban szükséges a `__dirname` eléréséhez 4. **IUserRepository** - A szülő interfész **Fontos ES6 Modules trükk:** ```javascript 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-t - `createdAt` - ISO formátumú timestamp - `users.push()` - Hozzáadja a tömb végéhez - `fs.writeFile()` - Visszamenti a fájlba - `JSON.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 → `null` visszaté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 `true` visszaadá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 ```javascript 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 Layer - 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`, `limit` paramé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 ```javascript 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:** 1. **Konstruktor:** - Fogad egy `userRepository` paramé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 2. **handle(query) metódus:** - Fogadja a Query objektumot (jelen esetben üres) - Meghívja a repository `getAll()` metódusát - Visszaadja az eredményt - `async/await` haszná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 ```javascript 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:** ```javascript 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 ```javascript 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:** 1. **Repository meghívása:** ```javascript 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 2. **Validáció:** ```javascript 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 3. **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 Layer nem tud HTTP-ről, csak üzleti hibákat dob
📄 Teljes kód megtekintése ```javascript 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 Layer - 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 `createdAt` timestamp-et sem - azt is a Repository adja hozzá - A Command csak az "üzleti adatokat" tartalmazza **Példa használat:** ```javascript 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 ```javascript 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):** ```javascript 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 Layer 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:** ```javascript 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 `undefined` ha nem adták meg - ez rendben van **3. Repository meghívása:** ```javascript 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 ```javascript 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év - `email` (string) - Az új/frissített email - `age` (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:** ```javascript const command = new UpdateUserCommand( '1707825600000', 'John Updated', 'john.new@example.com', 31 ); ```
📄 Teljes kód megtekintése ```javascript 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:** ```javascript 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:** ```javascript 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):** ```javascript if (!updatedUser) { throw new Error('User not found'); } ``` - Ha `null` jö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 ```javascript 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:** ```javascript 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 ```javascript 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:** ```javascript const deleted = await this.userRepository.delete(command.id); ``` - A repository `delete()` metódusa boolean-t ad vissza - `true` - sikeres törlés - `false` - nem létezett ilyen ID-jű user **2. Validáció:** ```javascript 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űv elet) - 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 ```javascript 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 Layer 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 layer-rel. **Részletes leírás:** A Controller a **Presentation Layer** része - ő az, aki tudja, mi az a HTTP, mi az a request és response. Az Application Layer-t (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:** ```javascript 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.body` adatokbó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 ```javascript 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 framework - `UserController` - A korábban létrehozott controller **2. Router és Controller példányosítás:** ```javascript 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:** ```javascript import express from 'express'; import userRouter from './routers/userRouter.js'; ``` - Express framework - User router (korábban definiált route-ok) **2. Express app instance:** ```javascript 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:** ```javascript 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:** ```javascript 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.body` undefined lenne - Az Express beépített middleware-je (korábban külön `body-parser` package volt) - Automatikusan parse-olja ha `Content-Type: application/json` **5. Route regisztráció:** ```javascript 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ában `GET /users` lesz - Moduláris: könnyen hozzáadhatunk más router-eket (pl. `/posts`, `/comments`) **Teljes útvonalak:** - `GET /users` → összes user - `GET /users/:id` → egy user - `POST /users` → új user - `PUT /users/:id` → user módosítás - `DELETE /users/:id` → user törlés **6. Szerver indítása:** ```javascript 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:** ```javascript 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 ```javascript 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}`); }); ```
trong> ```javascript 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` ```javascript 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}`); }); ``` **Magyarázat:** - **express.json()**: JSON body parser middleware (kötelező a POST/PUT kérésekhez) - **Base path**: `/users` prefix minden user route-hoz - **Port**: 3000 (módosítható szükség szerint) --- ## Futtatás és Tesztelés ### Szerver Indítása ```bash 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:** ```http GET http://localhost:3000/users ``` **Válasz:** ```json [] ``` #### 2. Új User Létrehozása **Request:** ```http POST http://localhost:3000/users Content-Type: application/json { "name": "John Doe", "email": "john@example.com", "age": 30 } ``` **Válasz:** ```json { "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:** ```http GET http://localhost:3000/users/1707825600000 ``` **Válasz:** ```json { "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:** ```http PUT http://localhost:3000/users/1707825600000 Content-Type: application/json { "name": "John Updated", "email": "john.updated@example.com", "age": 31 } ``` **Válasz:** ```json { "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:** ```http 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 ```javascript { "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` → `IUserRepository` interface-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**: ```javascript 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 :3000` majd `taskkill /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 layer: IUserRepository létrehozva - [ ] Infrastructure layer: UserRepository és user.json létrehozva - [ ] Application layer: Összes Query és Command létrehozva - [ ] Application layer: Összes Handler létrehozva - [ ] API layer: UserController létrehozva - [ ] API layer: userRouter létrehozva - [ ] API layer: 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