RestAPI
This commit is contained in:
@@ -0,0 +1,314 @@
|
|||||||
|
% REST API architektúra
|
||||||
|
|
||||||
|
\section{REST API Architektúra}
|
||||||
|
|
||||||
|
\begin{frame}{Miért van szükség architektúrára?}
|
||||||
|
\begin{block}{A probléma: "Spagetti kód"}
|
||||||
|
Kezdő fejlesztők gyakran mindent egy helyre írnak:
|
||||||
|
\begin{itemize}
|
||||||
|
\item Adatbázis lekérdezések a route-okban
|
||||||
|
\item Üzleti logika a controller-ekben
|
||||||
|
\item Validáció szétszórva mindenhol
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
\textbf{Eredmény:} Áttekinthetetlen, nehezen karbantartható kód
|
||||||
|
\end{block}
|
||||||
|
|
||||||
|
\begin{exampleblock}{A megoldás: Rétegezett architektúra}
|
||||||
|
Minden funkciónak megvan a saját helye és felelőssége.
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}{5-rétegű architektúra}
|
||||||
|
\begin{block}{Separation of Concerns - Felelősségek szétválasztása}
|
||||||
|
\begin{itemize}
|
||||||
|
\item \textbf{Karbantarthatóság} - Könnyebb módosítás, hibakeresés
|
||||||
|
\item \textbf{Tesztelhetőség} - Rétegek külön tesztelhetők
|
||||||
|
\item \textbf{Skálázhatóság (Scalability)} - Független fejlesztés és skálázás
|
||||||
|
\item \textbf{Újrafelhasználhatóság} - Rétegek több helyen használhatók
|
||||||
|
\end{itemize}
|
||||||
|
\end{block}
|
||||||
|
|
||||||
|
\begin{alertblock}{Alapelv}
|
||||||
|
Minden réteg csak a saját felelősségével foglalkozik!
|
||||||
|
\end{alertblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}{Az 5 réteg}
|
||||||
|
\begin{enumerate}
|
||||||
|
\item \textbf{API Layer (Presentation Layer)}
|
||||||
|
\begin{itemize}
|
||||||
|
\item HTTP kérések fogadása
|
||||||
|
\item Route-ok, Controller-ek
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
\item \textbf{Application Layer (Service Layer)}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Üzleti logika koordinálása
|
||||||
|
\item Service-ek
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
\item \textbf{Domain Layer (Business Logic)}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Üzleti szabályok
|
||||||
|
\item Model-ek, Entity-k
|
||||||
|
\end{itemize}
|
||||||
|
\end{enumerate}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}{Az 5 réteg (folyt.)}
|
||||||
|
\begin{enumerate}
|
||||||
|
\setcounter{enumi}{3}
|
||||||
|
\item \textbf{Infrastructure Layer}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Adatbázis hozzáférés
|
||||||
|
\item Repository-k, ORM
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
\item \textbf{Communication Layer}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Adatok átalakítása
|
||||||
|
\item DTO-k, Validáció
|
||||||
|
\end{itemize}
|
||||||
|
\end{enumerate}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[shrink=10]{Rétegek áttekintése - Diagram}
|
||||||
|
\begin{center}
|
||||||
|
\begin{tikzpicture}[node distance=0.3cm]
|
||||||
|
\node[draw, rectangle, fill=blue!20, minimum width=9cm, minimum height=1cm, align=center] (api) {\textbf{API réteg (API Layer)}\\Útvonalak, Vezérlők (Routes, Controllers)};
|
||||||
|
|
||||||
|
\node[draw, rectangle, fill=green!20, minimum width=9cm, minimum height=1cm, below=of api, align=center] (app) {\textbf{Alkalmazás réteg (Application Layer)}\\Szolgáltatások (Services)};
|
||||||
|
|
||||||
|
\node[draw, rectangle, fill=yellow!20, minimum width=9cm, minimum height=1cm, below=of app, align=center] (domain) {\textbf{Tartomány réteg (Domain Layer)}\\Modellek, Entitások (Models, Entities)};
|
||||||
|
|
||||||
|
\node[draw, rectangle, fill=red!20, minimum width=9cm, minimum height=1cm, below=of domain, align=center] (infra) {\textbf{Infrastruktúra réteg (Infrastructure Layer)}\\Tárolók, Adatbázis (Repository, DB)};
|
||||||
|
|
||||||
|
\node[draw, rectangle, fill=purple!20, minimum width=9cm, minimum height=1cm, below=of infra, align=center] (comm) {\textbf{Kommunikációs réteg (Communication Layer)}\\Adatátviteli objektumok, Ellenőrzés (DTOs, Validation)};
|
||||||
|
|
||||||
|
\draw[->, thick] (api.south) -- (app.north);
|
||||||
|
\draw[->, thick] (app.south) -- (domain.north);
|
||||||
|
\draw[->, thick] (domain.south) -- (infra.north);
|
||||||
|
\draw[<->, thick] (api.east) -- ++(1,0) |- (comm.east);
|
||||||
|
\end{tikzpicture}
|
||||||
|
\end{center}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=15]{API Layer - Route és Controller}
|
||||||
|
\begin{exampleblock}{Route példa}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
// routes/user.routes.js
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const userController = require('../controllers/user.controller');
|
||||||
|
|
||||||
|
router.get('/', userController.getAllUsers);
|
||||||
|
router.get('/:id', userController.getUserById);
|
||||||
|
router.post('/', userController.createUser);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=15]{API Layer - Controller}
|
||||||
|
\begin{exampleblock}{Controller példa}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
// controllers/user.controller.js
|
||||||
|
const userService = require('../services/user.service');
|
||||||
|
|
||||||
|
exports.getAllUsers = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const users = await userService.getAllUsers();
|
||||||
|
res.json(users);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.getUserById = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const user = await userService.getUserById(req.params.id);
|
||||||
|
if (!user) return res.status(404).json({ error: 'Not found' });
|
||||||
|
res.json(user);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=15]{Application Layer - Service}
|
||||||
|
\begin{exampleblock}{Service példa}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
// services/user.service.js
|
||||||
|
const userRepository = require('../repositories/user.repository');
|
||||||
|
|
||||||
|
exports.getAllUsers = async () => {
|
||||||
|
return await userRepository.findAll();
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.getUserById = async (id) => {
|
||||||
|
return await userRepository.findById(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.createUser = async (userData) => {
|
||||||
|
// Üzleti logika validálás
|
||||||
|
if (!userData.email || !userData.name) {
|
||||||
|
throw new Error('Email and name are required');
|
||||||
|
}
|
||||||
|
return await userRepository.create(userData);
|
||||||
|
};
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=15]{Domain Layer - Model/Entity}
|
||||||
|
\begin{exampleblock}{User Model}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
// models/user.model.js
|
||||||
|
class User {
|
||||||
|
constructor(id, name, email, createdAt) {
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
this.email = email;
|
||||||
|
this.createdAt = createdAt || new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Üzleti logika metódusok
|
||||||
|
isActive() {
|
||||||
|
return this.active === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasRole(role) {
|
||||||
|
return this.roles.includes(role);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = User;
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=15]{Infrastructure Layer - Repository}
|
||||||
|
\begin{exampleblock}{Repository példa}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
// repositories/user.repository.js
|
||||||
|
const db = require('../config/database');
|
||||||
|
|
||||||
|
exports.findAll = async () => {
|
||||||
|
return await db.query('SELECT * FROM users');
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.findById = async (id) => {
|
||||||
|
const result = await db.query('SELECT * FROM users WHERE id = ?', [id]);
|
||||||
|
return result[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.create = async (userData) => {
|
||||||
|
const result = await db.query(
|
||||||
|
'INSERT INTO users (name, email) VALUES (?, ?)',
|
||||||
|
[userData.name, userData.email]
|
||||||
|
);
|
||||||
|
return { id: result.insertId, ...userData };
|
||||||
|
};
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=15]{Communication Layer - DTO}
|
||||||
|
\begin{exampleblock}{Data Transfer Object}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
// dtos/user.dto.js
|
||||||
|
class UserDTO {
|
||||||
|
constructor(user) {
|
||||||
|
this.id = user.id;
|
||||||
|
this.name = user.name;
|
||||||
|
this.email = user.email;
|
||||||
|
// Érzékeny mezők nem kerülnek át (pl. password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CreateUserDTO {
|
||||||
|
constructor(data) {
|
||||||
|
this.name = data.name;
|
||||||
|
this.email = data.email;
|
||||||
|
this.password = data.password;
|
||||||
|
}
|
||||||
|
|
||||||
|
validate() {
|
||||||
|
if (!this.email || !this.name || !this.password) {
|
||||||
|
throw new Error('All fields required');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}{Projekt struktúra - Mappa felépítés}
|
||||||
|
\begin{block}{Az 5-rétegű architektúra a mappákban}
|
||||||
|
\small
|
||||||
|
\begin{itemize}
|
||||||
|
\item \textbf{\texttt{src/}} - Forráskód gyökér
|
||||||
|
\item \quad \texttt{routes/} - \textcolor{blue}{API réteg}: útvonalak definiálása
|
||||||
|
\item \quad \texttt{controllers/} - \textcolor{blue}{API réteg}: HTTP kérések kezelése
|
||||||
|
\item \quad \texttt{services/} - \textcolor{green}{Alkalmazás réteg}: üzleti logika
|
||||||
|
\item \quad \texttt{models/} - \textcolor{orange}{Tartomány réteg}: adatmodellek
|
||||||
|
\item \quad \texttt{repositories/} - \textcolor{red}{Infrastruktúra réteg}: DB műveletek
|
||||||
|
\item \quad \texttt{dtos/} - \textcolor{purple}{Kommunikáció réteg}: adatátviteli objektumok
|
||||||
|
\item \quad \texttt{middlewares/} - Köztes réteg (autentikáció, naplózás)
|
||||||
|
\item \quad \texttt{config/} - Konfigurációs fájlok
|
||||||
|
\end{itemize}
|
||||||
|
\end{block}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}{Projekt struktúra - Függőségek}
|
||||||
|
\begin{block}{Kommunikáció a rétegek között}
|
||||||
|
\begin{itemize}
|
||||||
|
\item \textbf{Routes} $\rightarrow$ Controllers (útvonal hívja a vezérlőt)
|
||||||
|
\item \textbf{Controllers} $\rightarrow$ Services (vezérlő hívja a szolgáltatást)
|
||||||
|
\item \textbf{Services} $\rightarrow$ Models + Repositories (szolgáltatás használja az adatréteget)
|
||||||
|
\item \textbf{Repositories} $\rightarrow$ Database (tároló eléri az adatbázist)
|
||||||
|
\item \textbf{DTOs} $\leftrightarrow$ Minden réteg (minden réteg használhat DTO-kat)
|
||||||
|
\end{itemize}
|
||||||
|
\end{block}
|
||||||
|
|
||||||
|
\begin{alertblock}{Fontos szabály}
|
||||||
|
A belső rétegek NEM függhetnek a külső rétegektől! A függőségek mindig befelé mutatnak.
|
||||||
|
\end{alertblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}{Clean Architecture}
|
||||||
|
\begin{block}{Dependency Rule}
|
||||||
|
Belső rétegek nem függhetnek külső rétegektől. A függőségek mindig befelé mutatnak.
|
||||||
|
\end{block}
|
||||||
|
|
||||||
|
\begin{exampleblock}{Előnyök}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Független a framework-től
|
||||||
|
\item Tesztelhető
|
||||||
|
\item Független az UI-tól
|
||||||
|
\item Független az adatbázistól
|
||||||
|
\item Független külső szolgáltatásoktól
|
||||||
|
\end{itemize}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}{Összefoglalás - Architektúra}
|
||||||
|
\begin{itemize}
|
||||||
|
\item 5-rétegű architektúra a clean code érdekében
|
||||||
|
\item API Layer (API réteg): Route-ok és Controller-ek (útvonalak, vezérlők)
|
||||||
|
\item Application Layer (Alkalmazás réteg): Service-ek (szolgáltatások - üzleti logika)
|
||||||
|
\item Domain Layer (Tartomány réteg): Model-ek és Entity-k (modellek, entitások)
|
||||||
|
\item Infrastructure Layer (Infrastruktúra réteg): Repository-k és DB (tárolók, adatbázis)
|
||||||
|
\item Communication Layer (Kommunikációs réteg): DTO-k és validáció (adatátvitel, ellenőrzés)
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
@@ -0,0 +1,329 @@
|
|||||||
|
% CQRS Pattern
|
||||||
|
|
||||||
|
\section{CQRS Pattern}
|
||||||
|
|
||||||
|
\begin{frame}{Mi a CQRS?}
|
||||||
|
\begin{block}{Command Query Responsibility Segregation}
|
||||||
|
\textbf{Magyarul:} Parancs-Lekérdezés Felelősség Szétválasztása
|
||||||
|
|
||||||
|
\vspace{0.3cm}
|
||||||
|
|
||||||
|
Az adatok \textbf{írásának} (módosítás) és \textbf{olvasásának} (lekérdezés) szétválasztása külön modellekre.
|
||||||
|
\end{block}
|
||||||
|
|
||||||
|
\begin{columns}
|
||||||
|
\begin{column}{0.48\textwidth}
|
||||||
|
\begin{exampleblock}{Command (Parancs)}
|
||||||
|
\textbf{Írási műveletek:}
|
||||||
|
\begin{itemize}
|
||||||
|
\item POST, PUT, PATCH, DELETE
|
||||||
|
\item Adatok módosítása
|
||||||
|
\item Validáció, üzleti logika
|
||||||
|
\item Pl: felhasználó létrehozása
|
||||||
|
\end{itemize}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{column}
|
||||||
|
|
||||||
|
\begin{column}{0.48\textwidth}
|
||||||
|
\begin{exampleblock}{Query (Lekérdezés)}
|
||||||
|
\textbf{Olvasási műveletek:}
|
||||||
|
\begin{itemize}
|
||||||
|
\item GET
|
||||||
|
\item Adatok lekérdezése
|
||||||
|
\item Optimalizált olvasás
|
||||||
|
\item Pl: felhasználók listázása
|
||||||
|
\end{itemize}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{column}
|
||||||
|
\end{columns}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}{Miért van szükség CQRS-re?}
|
||||||
|
\begin{block}{A probléma}
|
||||||
|
Hagyományos CRUD alkalmazásokban ugyanazt a modellt használjuk írásra és olvasásra.
|
||||||
|
Ez problémás lehet:
|
||||||
|
\begin{itemize}
|
||||||
|
\item Az írási és olvasási igények eltérőek
|
||||||
|
\item Az olvasás gyakran bonyolultabb (join-ok, aggregációk)
|
||||||
|
\item A teljesítmény-optimalizálás nehéz
|
||||||
|
\end{itemize}
|
||||||
|
\end{block}
|
||||||
|
|
||||||
|
\begin{exampleblock}{A megoldás: CQRS}
|
||||||
|
Külön modellek külön optimalizálással:
|
||||||
|
\begin{itemize}
|
||||||
|
\item \textbf{Write Model} (Író modell): validáció, üzleti szabályok
|
||||||
|
\item \textbf{Read Model} (Olvasó modell): gyors lekérdezések, cache
|
||||||
|
\end{itemize}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}{CQRS előnyei}
|
||||||
|
\begin{block}{Előnyök}
|
||||||
|
\begin{itemize}
|
||||||
|
\item \textbf{Független optimalizálás} - Az írás és olvasás külön optimalizálható
|
||||||
|
\begin{itemize}
|
||||||
|
\item Íráshoz: tranzakciók, validáció, konzisztencia
|
||||||
|
\item Olvasáshoz: cache, denormalizált adatok, gyors lekérdezések
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
\item \textbf{Skálázhatóság (Scalability)} - Read és Write modellek külön skálázhatók
|
||||||
|
\begin{itemize}
|
||||||
|
\item Több olvasó szerver, kevesebb író szerver
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
\item \textbf{Egyszerűség} - Komplexitás csökken
|
||||||
|
\begin{itemize}
|
||||||
|
\item Minden modell egy feladatra fókuszál
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
\item \textbf{Teljesítmény (Performance)} - Gyorsabb lekérdezések
|
||||||
|
\end{itemize}
|
||||||
|
\end{block}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}{Mikor használjuk a CQRS-t?}
|
||||||
|
\begin{exampleblock}{Jó választás, ha:}
|
||||||
|
\begin{itemize}
|
||||||
|
\item \textbf{Komplex domain logika} van az alkalmazásban
|
||||||
|
\item \textbf{Nagy terhelés} várható (sok párhuzamos felhasználó)
|
||||||
|
\item \textbf{Eltérő írás/olvasás igények}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Pl: ritkán írunk, sokszor olvasunk
|
||||||
|
\end{itemize}
|
||||||
|
\item \textbf{Különböző adatformátumok} kellenek íráshoz és olvasáshoz
|
||||||
|
\end{itemize}
|
||||||
|
\end{exampleblock}
|
||||||
|
|
||||||
|
\begin{alertblock}{NEM használjuk, ha:}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Egyszerű CRUD alkalmazás
|
||||||
|
\item Kis terhelés
|
||||||
|
\item Az írás és olvasás hasonló
|
||||||
|
\end{itemize}
|
||||||
|
\end{alertblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[shrink=10]{CRUD vs CQRS - Architektúra összehasonlítás}
|
||||||
|
\begin{center}
|
||||||
|
\begin{tikzpicture}[node distance=1.5cm]
|
||||||
|
% CRUD
|
||||||
|
\node[draw, rectangle, fill=blue!20, minimum width=2.5cm, minimum height=0.9cm, align=center] (crud) {\small CRUD\\Modell};
|
||||||
|
\node[above=0.3cm of crud, font=\bfseries] {Hagyományos};
|
||||||
|
\node[draw, rectangle, fill=green!20, minimum width=2.5cm, minimum height=0.9cm, right=1.2cm of crud, align=center] (db1) {\small Adatbázis\\(Database)};
|
||||||
|
\draw[<->, thick] (crud) -- (db1) node[midway, above, font=\tiny] {olvas/ír};
|
||||||
|
|
||||||
|
% CQRS
|
||||||
|
\node[draw, rectangle, fill=yellow!20, minimum width=2.5cm, minimum height=0.9cm, below=2cm of crud, align=center] (cmd) {\small Parancs\\(Command)};
|
||||||
|
\node[draw, rectangle, fill=orange!20, minimum width=2.5cm, minimum height=0.9cm, below=0.6cm of cmd, align=center] (query) {\small Lekérdezés\\(Query)};
|
||||||
|
\node[above=0.3cm of cmd, font=\bfseries] {CQRS};
|
||||||
|
\node[draw, rectangle, fill=green!20, minimum width=2.5cm, minimum height=0.9cm, right=1.2cm of cmd, align=center] (db2) {\small Író DB\\(Write)};
|
||||||
|
\node[draw, rectangle, fill=green!20, minimum width=2.5cm, minimum height=0.9cm, right=1.2cm of query, align=center] (db3) {\small Olvasó DB\\(Read)};
|
||||||
|
|
||||||
|
\draw[->, thick] (cmd) -- (db2) node[midway, above, font=\tiny] {ír};
|
||||||
|
\draw[<-, thick] (query) -- (db3) node[midway, above, font=\tiny] {olvas};
|
||||||
|
\draw[->, dashed, thick] (db2) -- (db3) node[midway, right, font=\tiny] {szinkron};
|
||||||
|
\end{tikzpicture}
|
||||||
|
\end{center}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=15]{Command - Írás példa}
|
||||||
|
\begin{exampleblock}{CreateUserCommand}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
// commands/create-user.command.js
|
||||||
|
class CreateUserCommand {
|
||||||
|
constructor(name, email, password) {
|
||||||
|
this.name = name;
|
||||||
|
this.email = email;
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
validate() {
|
||||||
|
if (!this.email || !this.name || !this.password) {
|
||||||
|
throw new Error('All fields required');
|
||||||
|
}
|
||||||
|
if (this.password.length < 8) {
|
||||||
|
throw new Error('Password too short');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = CreateUserCommand;
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=15]{Command Handler}
|
||||||
|
\begin{exampleblock}{CreateUserCommandHandler}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
// handlers/create-user.handler.js
|
||||||
|
const userRepository = require('../repositories/user.repository');
|
||||||
|
|
||||||
|
class CreateUserCommandHandler {
|
||||||
|
async handle(command) {
|
||||||
|
command.validate();
|
||||||
|
|
||||||
|
// Üzleti logika
|
||||||
|
const existingUser = await userRepository.findByEmail(command.email);
|
||||||
|
if (existingUser) {
|
||||||
|
throw new Error('Email already exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Password hash
|
||||||
|
const hashedPassword = await bcrypt.hash(command.password, 10);
|
||||||
|
|
||||||
|
// User létrehozás
|
||||||
|
const user = await userRepository.create({
|
||||||
|
name: command.name,
|
||||||
|
email: command.email,
|
||||||
|
password: hashedPassword
|
||||||
|
});
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=15]{Query - Olvasás példa}
|
||||||
|
\begin{exampleblock}{GetUsersQuery}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
// queries/get-users.query.js
|
||||||
|
class GetUsersQuery {
|
||||||
|
constructor(filters = {}) {
|
||||||
|
this.page = filters.page || 1;
|
||||||
|
this.limit = filters.limit || 10;
|
||||||
|
this.role = filters.role;
|
||||||
|
this.active = filters.active;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlers/get-users.handler.js
|
||||||
|
class GetUsersQueryHandler {
|
||||||
|
async handle(query) {
|
||||||
|
// Optimalizált lekérdezés
|
||||||
|
const users = await userReadModel.find({
|
||||||
|
role: query.role,
|
||||||
|
active: query.active,
|
||||||
|
skip: (query.page - 1) * query.limit,
|
||||||
|
limit: query.limit
|
||||||
|
});
|
||||||
|
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=15]{CQRS REST API-ban (1/2)}
|
||||||
|
\begin{exampleblock}{Command végpontok}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
// routes/user.routes.js
|
||||||
|
const CreateUserCommand = require('../commands/create-user.command');
|
||||||
|
const createUserHandler = require('../handlers/create-user.handler');
|
||||||
|
|
||||||
|
router.post('/users', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const command = new CreateUserCommand(
|
||||||
|
req.body.name,
|
||||||
|
req.body.email,
|
||||||
|
req.body.password
|
||||||
|
);
|
||||||
|
const user = await createUserHandler.handle(command);
|
||||||
|
res.status(201).json(user);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=15]{CQRS REST API-ban (2/2)}
|
||||||
|
\begin{exampleblock}{Query végpontok}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
const GetUsersQuery = require('../queries/get-users.query');
|
||||||
|
const getUsersHandler = require('../handlers/get-users.handler');
|
||||||
|
|
||||||
|
router.get('/users', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const query = new GetUsersQuery({
|
||||||
|
page: req.query.page,
|
||||||
|
limit: req.query.limit,
|
||||||
|
role: req.query.role,
|
||||||
|
active: req.query.active
|
||||||
|
});
|
||||||
|
const users = await getUsersHandler.handle(query);
|
||||||
|
res.json(users);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}{CQRS előnyei és hátrányai}
|
||||||
|
\begin{columns}
|
||||||
|
\begin{column}{0.48\textwidth}
|
||||||
|
\begin{block}{Előnyök}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Jobb teljesítmény
|
||||||
|
\item Független skálázás
|
||||||
|
\item Egyszerűbb modellek
|
||||||
|
\item Optimalizált lekérdezések
|
||||||
|
\end{itemize}
|
||||||
|
\end{block}
|
||||||
|
\end{column}
|
||||||
|
|
||||||
|
\begin{column}{0.48\textwidth}
|
||||||
|
\begin{alertblock}{Hátrányok}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Komplexitás növekedés
|
||||||
|
\item Több kód
|
||||||
|
\item Szinkronizáció problémák
|
||||||
|
\item Eventual consistency
|
||||||
|
\end{itemize}
|
||||||
|
\end{alertblock}
|
||||||
|
\end{column}
|
||||||
|
\end{columns}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}{Event Sourcing + CQRS}
|
||||||
|
\begin{block}{Event Sourcing}
|
||||||
|
Az állapotváltozások eseményekként tárolása, nem a végállapot mentése.
|
||||||
|
\end{block}
|
||||||
|
|
||||||
|
\begin{exampleblock}{Események}
|
||||||
|
\begin{itemize}
|
||||||
|
\item UserCreatedEvent
|
||||||
|
\item UserEmailChangedEvent
|
||||||
|
\item UserDeactivatedEvent
|
||||||
|
\end{itemize}
|
||||||
|
\end{exampleblock}
|
||||||
|
|
||||||
|
\begin{alertblock}{CQRS + ES kombinációja}
|
||||||
|
Command-ok eseményeket generálnak, Query modellek eseményekből épülnek fel.
|
||||||
|
\end{alertblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}{Összefoglalás - CQRS}
|
||||||
|
\begin{itemize}
|
||||||
|
\item CQRS = Command és Query szétválasztása
|
||||||
|
\item Command: adatok módosítása (POST, PUT, DELETE)
|
||||||
|
\item Query: adatok lekérdezése (GET)
|
||||||
|
\item Előnyök: teljesítmény, skálázhatóság
|
||||||
|
\item Hátrányok: komplexitás
|
||||||
|
\item Event Sourcing jól kombinálható CQRS-sel
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
\vspace{0.5cm}
|
||||||
|
|
||||||
|
\end{frame}
|
||||||
@@ -0,0 +1,339 @@
|
|||||||
|
% Express.js REST API
|
||||||
|
|
||||||
|
\section{Express.js REST API}
|
||||||
|
|
||||||
|
\begin{frame}{Mi az Express.js?}
|
||||||
|
\begin{block}{Express}
|
||||||
|
Gyors, minimalista web framework Node.js-hez.
|
||||||
|
\end{block}
|
||||||
|
|
||||||
|
\begin{exampleblock}{Jellemzők}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Egyszerű API
|
||||||
|
\item Middleware-alapú
|
||||||
|
\item Routing támogatás
|
||||||
|
\item Template engine-ek
|
||||||
|
\item Nagy közösség és sok plugin
|
||||||
|
\end{itemize}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=10]{Express telepítése}
|
||||||
|
\begin{exampleblock}{NPM inicializálás és telepítés}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
npm init -y
|
||||||
|
npm install express
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
|
||||||
|
\begin{exampleblock}{package.json}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
{
|
||||||
|
"name": "my-api",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js",
|
||||||
|
"dev": "nodemon server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.18.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=10]{Első Express szerver}
|
||||||
|
\begin{exampleblock}{server.js}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
const express = require('express');
|
||||||
|
const app = express();
|
||||||
|
const PORT = 3000;
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Egyszerű route
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.json({ message: 'Hello, Express!' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Szerver indítása
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Server running on http://localhost:${PORT}`);
|
||||||
|
});
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=10]{GET végpont}
|
||||||
|
\begin{exampleblock}{Összes elem lekérdezése}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
let users = [
|
||||||
|
{id: 1, name: 'Alice', email: 'alice@example.com'},
|
||||||
|
{id: 2, name: 'Bob', email: 'bob@example.com'}
|
||||||
|
];
|
||||||
|
|
||||||
|
app.get('/api/users', (req, res) => {
|
||||||
|
res.json(users);
|
||||||
|
});
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
|
||||||
|
\begin{exampleblock}{Egy elem lekérdezése}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
app.get('/api/users/:id', (req, res) => {
|
||||||
|
const user = users.find(u => u.id === parseInt(req.params.id));
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({ error: 'User not found' });
|
||||||
|
}
|
||||||
|
res.json(user);
|
||||||
|
});
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=10]{POST végpont}
|
||||||
|
\begin{exampleblock}{Új elem létrehozása}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
app.post('/api/users', (req, res) => {
|
||||||
|
const { name, email } = req.body;
|
||||||
|
|
||||||
|
if (!name || !email) {
|
||||||
|
return res.status(400).json({ error: 'Name and email required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const newUser = {
|
||||||
|
id: users.length + 1,
|
||||||
|
name,
|
||||||
|
email
|
||||||
|
};
|
||||||
|
|
||||||
|
users.push(newUser);
|
||||||
|
res.status(201).json(newUser);
|
||||||
|
});
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=10]{PUT és PATCH végpontok}
|
||||||
|
\begin{exampleblock}{PUT - Teljes frissítés}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
app.put('/api/users/:id', (req, res) => {
|
||||||
|
const idx = users.findIndex(u => u.id === parseInt(req.params.id));
|
||||||
|
if (idx === -1) return res.status(404).json({ error: 'Not found' });
|
||||||
|
|
||||||
|
users[idx] = { id: parseInt(req.params.id), ...req.body };
|
||||||
|
res.json(users[idx]);
|
||||||
|
});
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
|
||||||
|
\begin{exampleblock}{PATCH - Részleges frissítés}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
app.patch('/api/users/:id', (req, res) => {
|
||||||
|
const user = users.find(u => u.id === parseInt(req.params.id));
|
||||||
|
if (!user) return res.status(404).json({ error: 'Not found' });
|
||||||
|
|
||||||
|
Object.assign(user, req.body);
|
||||||
|
res.json(user);
|
||||||
|
});
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=10]{DELETE végpont}
|
||||||
|
\begin{exampleblock}{Elem törlése}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
app.delete('/api/users/:id', (req, res) => {
|
||||||
|
const idx = users.findIndex(u => u.id === parseInt(req.params.id));
|
||||||
|
|
||||||
|
if (idx === -1) {
|
||||||
|
return res.status(404).json({ error: 'User not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
users.splice(idx, 1);
|
||||||
|
res.status(204).send();
|
||||||
|
});
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
|
||||||
|
\begin{alertblock}{204 No Content}
|
||||||
|
Sikeres törlés esetén nem kell response body.
|
||||||
|
\end{alertblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=10]{Middleware-ek}
|
||||||
|
\begin{block}{Mi a Middleware?}
|
||||||
|
Függvények, amelyek hozzáférnek a request és response objektumokhoz, és a \texttt{next()} függvényhez.
|
||||||
|
\end{block}
|
||||||
|
|
||||||
|
\begin{exampleblock}{Logger middleware}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
console.log(`${req.method} ${req.url}`);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
|
||||||
|
\begin{exampleblock}{Auth middleware}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
const authMiddleware = (req, res, next) => {
|
||||||
|
const token = req.headers.authorization;
|
||||||
|
if (!token) return res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
|
app.get('/api/protected', authMiddleware, (req, res) => {
|
||||||
|
res.json({ data: 'Protected data' });
|
||||||
|
});
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=10]{Error Handling}
|
||||||
|
\begin{exampleblock}{Error handling middleware}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
app.use((err, req, res, next) => {
|
||||||
|
console.error(err.stack);
|
||||||
|
res.status(500).json({
|
||||||
|
error: {
|
||||||
|
message: err.message,
|
||||||
|
code: 'INTERNAL_ERROR'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
|
||||||
|
\begin{exampleblock}{Try-catch async}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
app.get('/api/users/:id', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const user = await getUserById(req.params.id);
|
||||||
|
if (!user) throw new Error('User not found');
|
||||||
|
res.json(user);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=10]{Validáció Joi-val (1/2)}
|
||||||
|
\begin{exampleblock}{Joi telepítése}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
npm install joi
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
|
||||||
|
\begin{exampleblock}{Schema definiálás}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
const Joi = require('joi');
|
||||||
|
|
||||||
|
const userSchema = Joi.object({
|
||||||
|
name: Joi.string().min(3).max(50).required(),
|
||||||
|
email: Joi.string().email().required(),
|
||||||
|
age: Joi.number().integer().min(0).max(120)
|
||||||
|
});
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=10]{Validáció Joi-val (2/2)}
|
||||||
|
\begin{exampleblock}{Validáció használata}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
app.post('/api/users', (req, res) => {
|
||||||
|
const { error, value } = userSchema.validate(req.body);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: error.details[0].message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// value a validált adat
|
||||||
|
const newUser = { id: users.length + 1, ...value };
|
||||||
|
users.push(newUser);
|
||||||
|
res.status(201).json(newUser);
|
||||||
|
});
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=15]{Express Router - Moduláris routing (1/2)}
|
||||||
|
\begin{exampleblock}{routes/users.js}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/', (req, res) => {
|
||||||
|
res.json({ message: 'Get all users' });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/:id', (req, res) => {
|
||||||
|
res.json({ message: `Get user ${req.params.id}` });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/', (req, res) => {
|
||||||
|
res.json({ message: 'Create user' });
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=10]{Express Router - Moduláris routing (2/2)}
|
||||||
|
\begin{exampleblock}{app.js - Router használata}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
const express = require('express');
|
||||||
|
const app = express();
|
||||||
|
const userRoutes = require('./routes/users');
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
app.use('/api/users', userRoutes);
|
||||||
|
|
||||||
|
app.listen(3000);
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
|
||||||
|
\begin{alertblock}{Prefix}
|
||||||
|
Az \texttt{app.use('/api/users', userRoutes)} prefix-szel látja el az összes route-ot.
|
||||||
|
\end{alertblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}{Összefoglalás - Express}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Express = minimalista web framework
|
||||||
|
\item Routing: GET, POST, PUT, PATCH, DELETE
|
||||||
|
\item Middleware-ek: kérések feldolgozása
|
||||||
|
\item Error handling: hibakezelő middleware
|
||||||
|
\item Validáció: Joi vagy más library
|
||||||
|
\item Router: moduláris útvonalkezelés
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
\vspace{0.5cm}
|
||||||
|
|
||||||
|
\end{frame}
|
||||||
@@ -0,0 +1,354 @@
|
|||||||
|
% Postman
|
||||||
|
|
||||||
|
\section{API tesztelés Postman-nel}
|
||||||
|
|
||||||
|
\begin{frame}{Mi a Postman?}
|
||||||
|
\begin{block}{Postman}
|
||||||
|
API fejlesztéshez és teszteléshez használt platform.
|
||||||
|
\end{block}
|
||||||
|
|
||||||
|
\begin{exampleblock}{Funkciók}
|
||||||
|
\begin{itemize}
|
||||||
|
\item HTTP kérések küldése
|
||||||
|
\item Collection-ök létrehozása
|
||||||
|
\item Environment változók
|
||||||
|
\item Automatikus tesztek
|
||||||
|
\item API dokumentáció generálás
|
||||||
|
\item Csapatmunka támogatás
|
||||||
|
\end{itemize}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}{Postman telepítése}
|
||||||
|
\begin{enumerate}
|
||||||
|
\item \textbf{Weboldal}: \url{https://www.postman.com/downloads/}
|
||||||
|
\item \textbf{Letöltés}: Desktop app (Windows, Mac, Linux)
|
||||||
|
\item \textbf{Telepítés}: Installer futtatása
|
||||||
|
\item \textbf{Bejelentkezés}: Ingyenes fiók létrehozása (opcionális)
|
||||||
|
\end{enumerate}
|
||||||
|
|
||||||
|
\vspace{0.5cm}
|
||||||
|
|
||||||
|
\begin{alertblock}{Web verzió}
|
||||||
|
Használható böngészőben is, de a desktop app gyorsabb.
|
||||||
|
\end{alertblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}{Első kérés Postman-ben}
|
||||||
|
\begin{enumerate}
|
||||||
|
\item \textbf{New} $\rightarrow$ \textbf{HTTP Request}
|
||||||
|
\item \textbf{Metódus} kiválasztása: GET, POST, PUT, DELETE
|
||||||
|
\item \textbf{URL} megadása: \texttt{http://localhost:3000/api/users}
|
||||||
|
\item \textbf{Send} gomb
|
||||||
|
\item \textbf{Response} megjelenik alul
|
||||||
|
\end{enumerate}
|
||||||
|
|
||||||
|
\vspace{0.3cm}
|
||||||
|
|
||||||
|
\begin{exampleblock}{Példa}
|
||||||
|
GET \texttt{http://localhost:3000/api/users}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[shrink=10]{HTTP metódusok Postman-ben}
|
||||||
|
\begin{columns}
|
||||||
|
\begin{column}{0.48\textwidth}
|
||||||
|
\begin{block}{GET kérés}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Metódus: GET
|
||||||
|
\item URL: /api/users
|
||||||
|
\item Body nem kell
|
||||||
|
\end{itemize}
|
||||||
|
\end{block}
|
||||||
|
|
||||||
|
\begin{block}{POST kérés}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Metódus: POST
|
||||||
|
\item URL: /api/users
|
||||||
|
\item Body tab $\rightarrow$ JSON
|
||||||
|
\end{itemize}
|
||||||
|
\end{block}
|
||||||
|
\end{column}
|
||||||
|
|
||||||
|
\begin{column}{0.48\textwidth}
|
||||||
|
\begin{block}{PUT kérés}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Metódus: PUT
|
||||||
|
\item URL: /api/users/:id
|
||||||
|
\item Body tab $\rightarrow$ JSON
|
||||||
|
\end{itemize}
|
||||||
|
\end{block}
|
||||||
|
|
||||||
|
\begin{block}{DELETE kérés}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Metódus: DELETE
|
||||||
|
\item URL: /api/users/:id
|
||||||
|
\item Body nem kell
|
||||||
|
\end{itemize}
|
||||||
|
\end{block}
|
||||||
|
\end{column}
|
||||||
|
\end{columns}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[shrink=10]{Request Body típusok}
|
||||||
|
\begin{enumerate}
|
||||||
|
\item \textbf{none} - Nincs body (GET, DELETE)
|
||||||
|
|
||||||
|
\item \textbf{form-data} - Multipart form (fájl feltöltés)
|
||||||
|
|
||||||
|
\item \textbf{x-www-form-urlencoded} - URL encoded form
|
||||||
|
|
||||||
|
\item \textbf{raw} - Nyers adat
|
||||||
|
\begin{itemize}
|
||||||
|
\item JSON (leggyakoribb)
|
||||||
|
\item Text, XML, HTML
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
\item \textbf{binary} - Fájl feltöltés
|
||||||
|
|
||||||
|
\item \textbf{GraphQL} - GraphQL query
|
||||||
|
\end{enumerate}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=10]{Headers beállítása}
|
||||||
|
\begin{exampleblock}{Headers tab}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer YOUR_TOKEN_HERE
|
||||||
|
Accept: application/json
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
|
||||||
|
\begin{alertblock}{Fontos header-ek}
|
||||||
|
\begin{itemize}
|
||||||
|
\item \textbf{Content-Type} - Body formátuma
|
||||||
|
\item \textbf{Authorization} - Autentikáció
|
||||||
|
\item \textbf{Accept} - Milyen formátumot várunk
|
||||||
|
\end{itemize}
|
||||||
|
\end{alertblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}{Collection-ök}
|
||||||
|
\begin{block}{Mi a Collection?}
|
||||||
|
Kapcsolódó API kérések csoportosítása egy helyen.
|
||||||
|
\end{block}
|
||||||
|
|
||||||
|
\begin{exampleblock}{Collection struktúra}
|
||||||
|
\begin{itemize}
|
||||||
|
\item \textbf{Users}
|
||||||
|
\begin{itemize}
|
||||||
|
\item GET All Users
|
||||||
|
\item GET User by ID
|
||||||
|
\item POST Create User
|
||||||
|
\item PUT Update User
|
||||||
|
\item DELETE User
|
||||||
|
\end{itemize}
|
||||||
|
\item \textbf{Products}
|
||||||
|
\item \textbf{Authentication}
|
||||||
|
\end{itemize}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}{Collection létrehozása}
|
||||||
|
\begin{enumerate}
|
||||||
|
\item \textbf{Collections} $\rightarrow$ \textbf{+} (New Collection)
|
||||||
|
\item \textbf{Név}: pl. "My REST API"
|
||||||
|
\item \textbf{Add a request} $\rightarrow$ kérések hozzáadása
|
||||||
|
\item \textbf{Mappa}: Kérések mappákba szervezése
|
||||||
|
\item \textbf{Save} - Minden kérést menteni kell
|
||||||
|
\end{enumerate}
|
||||||
|
|
||||||
|
\vspace{0.3cm}
|
||||||
|
|
||||||
|
\begin{alertblock}{Előnyök}
|
||||||
|
Gyors hozzáférés, újrafelhasználás, megosztás csapattal.
|
||||||
|
\end{alertblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[shrink=10]{Environment Variables}
|
||||||
|
\begin{block}{Miért?}
|
||||||
|
Különböző környezetek (dev, staging, prod) közötti váltás anélkül, hogy minden kérést módosítanánk.
|
||||||
|
\end{block}
|
||||||
|
|
||||||
|
\begin{exampleblock}{Environment létrehozása}
|
||||||
|
\begin{enumerate}
|
||||||
|
\item Environments $\rightarrow$ + New Environment
|
||||||
|
\item Név: \texttt{Development}
|
||||||
|
\item Változók:
|
||||||
|
\begin{itemize}
|
||||||
|
\item \texttt{baseUrl}: \texttt{http://localhost:3000}
|
||||||
|
\item \texttt{apiKey}: \texttt{dev-key-123}
|
||||||
|
\item \texttt{token}: \texttt{eyJhbGc...}
|
||||||
|
\end{itemize}
|
||||||
|
\item Save
|
||||||
|
\end{enumerate}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[shrink=10]{Environment használata}
|
||||||
|
\begin{exampleblock}{Változó használat}
|
||||||
|
\begin{itemize}
|
||||||
|
\item URL: \texttt{\{\{baseUrl\}\}/api/users}
|
||||||
|
\item Header: \texttt{Authorization: Bearer \{\{token\}\}}
|
||||||
|
\item Body: \texttt{\{"userId": "\{\{userId\}\}"\}}
|
||||||
|
\end{itemize}
|
||||||
|
\end{exampleblock}
|
||||||
|
|
||||||
|
\begin{exampleblock}{Váltás környezetek között}
|
||||||
|
Jobb felső sarok $\rightarrow$ Environment kiválasztása
|
||||||
|
\begin{itemize}
|
||||||
|
\item Development
|
||||||
|
\item Staging
|
||||||
|
\item Production
|
||||||
|
\end{itemize}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=15]{Tests (Tesztek) (1/2)}
|
||||||
|
\begin{block}{Mi a Test?}
|
||||||
|
JavaScript kód, amely a válasz után fut és ellenőrzi azt.
|
||||||
|
\end{block}
|
||||||
|
|
||||||
|
\begin{exampleblock}{Tests tab - Alapvető tesztek}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
// Status code
|
||||||
|
pm.test("Status code is 200", function () {
|
||||||
|
pm.response.to.have.status(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Response time
|
||||||
|
pm.test("Response time < 200ms", function () {
|
||||||
|
pm.expect(pm.response.responseTime).to.be.below(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
// JSON response
|
||||||
|
pm.test("Response is JSON", function () {
|
||||||
|
pm.response.to.be.json;
|
||||||
|
});
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=15]{Tests (Tesztek) (2/2)}
|
||||||
|
\begin{exampleblock}{JSON response ellenőrzés}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
const response = pm.response.json();
|
||||||
|
|
||||||
|
pm.test("Name is John", function () {
|
||||||
|
pm.expect(response.name).to.eql("John Doe");
|
||||||
|
});
|
||||||
|
|
||||||
|
pm.test("Has email field", function () {
|
||||||
|
pm.expect(response).to.have.property("email");
|
||||||
|
});
|
||||||
|
|
||||||
|
pm.test("Email is valid", function () {
|
||||||
|
pm.expect(response.email).to.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
|
||||||
|
});
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
|
||||||
|
\begin{alertblock}{Test Results}
|
||||||
|
Send után megjelenik a Tests panel az eredménnyel.
|
||||||
|
\end{alertblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=15]{Pre-request Scripts}
|
||||||
|
\begin{block}{Mi az?}
|
||||||
|
JavaScript kód, amely a kérés előtt fut le.
|
||||||
|
\end{block}
|
||||||
|
|
||||||
|
\begin{exampleblock}{Példák}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
// Timestamp generálás
|
||||||
|
pm.environment.set("timestamp", Date.now());
|
||||||
|
|
||||||
|
// Random szám
|
||||||
|
pm.environment.set("randomNumber", Math.floor(Math.random() * 1000));
|
||||||
|
|
||||||
|
// UUID generálás
|
||||||
|
pm.environment.set("uuid", pm.variables.replaceIn('{{$guid}}'));
|
||||||
|
|
||||||
|
// Változó beállítása
|
||||||
|
pm.environment.set("userId", 123);
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=15]{Változók mentése Tests-ből}
|
||||||
|
\begin{exampleblock}{Response-ból változó mentése}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
const response = pm.response.json();
|
||||||
|
|
||||||
|
// Token mentése
|
||||||
|
pm.environment.set("token", response.token);
|
||||||
|
|
||||||
|
// User ID mentése
|
||||||
|
pm.environment.set("userId", response.id);
|
||||||
|
|
||||||
|
// Használat következő kérésben
|
||||||
|
// URL: {{baseUrl}}/api/users/{{userId}}
|
||||||
|
// Header: Authorization: Bearer {{token}}
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
|
||||||
|
\begin{alertblock}{Workflow}
|
||||||
|
Login $\rightarrow$ token mentése $\rightarrow$ Protected endpoint hívás
|
||||||
|
\end{alertblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}{Collection Runner}
|
||||||
|
\begin{block}{Mi az?}
|
||||||
|
Collection-ök automatikus futtatása sorban.
|
||||||
|
\end{block}
|
||||||
|
|
||||||
|
\begin{exampleblock}{Használat}
|
||||||
|
\begin{enumerate}
|
||||||
|
\item Collection $\rightarrow$ Run
|
||||||
|
\item Kérések sorrendje beállítása
|
||||||
|
\item Iterations: hányszor fusson
|
||||||
|
\item Delay: várakozás kérések között
|
||||||
|
\item Run Collection
|
||||||
|
\end{enumerate}
|
||||||
|
\end{exampleblock}
|
||||||
|
|
||||||
|
\begin{alertblock}{Előny}
|
||||||
|
Teljes API tesztelése egy gombnyomással.
|
||||||
|
\end{alertblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}{Dokumentáció generálás}
|
||||||
|
\begin{block}{Automatic documentation}
|
||||||
|
Postman automatikusan generál dokumentációt Collection-ből.
|
||||||
|
\end{block}
|
||||||
|
|
||||||
|
\begin{exampleblock}{Lépések}
|
||||||
|
\begin{enumerate}
|
||||||
|
\item Collection $\rightarrow$ View Documentation
|
||||||
|
\item Publish documentation (opcionális)
|
||||||
|
\item Megosztás csapattal vagy publikusan
|
||||||
|
\end{enumerate}
|
||||||
|
\end{exampleblock}
|
||||||
|
|
||||||
|
\begin{alertblock}{Előny}
|
||||||
|
Mindig naprakész dokumentáció a Collection alapján.
|
||||||
|
\end{alertblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}{Összefoglalás - Postman}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Postman = API fejlesztési platform
|
||||||
|
\item HTTP kérések küldése és tesztelése
|
||||||
|
\item Collection-ök: kérések csoportosítása
|
||||||
|
\item Environment: változók különböző környezetekhez
|
||||||
|
\item Tests: automatikus válasz ellenőrzés
|
||||||
|
\item Pre-request Scripts: kérés előtti logika
|
||||||
|
\item Collection Runner: teljes API tesztelés
|
||||||
|
\item Dokumentáció: automatikus generálás
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
@@ -0,0 +1,345 @@
|
|||||||
|
% Routing
|
||||||
|
|
||||||
|
\section{Routing}
|
||||||
|
|
||||||
|
\begin{frame}{Mi a Routing?}
|
||||||
|
\begin{block}{Routing}
|
||||||
|
A kérések irányítása a megfelelő végpontokra (endpoint).
|
||||||
|
\end{block}
|
||||||
|
|
||||||
|
\begin{exampleblock}{Route komponensek}
|
||||||
|
\begin{itemize}
|
||||||
|
\item \textbf{HTTP metódus} - GET, POST, PUT, DELETE
|
||||||
|
\item \textbf{URL path} - /api/users, /api/products/:id
|
||||||
|
\item \textbf{Handler függvény} - (req, res) => \{\}
|
||||||
|
\end{itemize}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=10]{Alapvető routing}
|
||||||
|
\begin{exampleblock}{Egyszerű route-ok}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
app.get('/api/users', (req, res) => {
|
||||||
|
res.json({ message: 'Get all users' });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/users', (req, res) => {
|
||||||
|
res.json({ message: 'Create user' });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.put('/api/users/:id', (req, res) => {
|
||||||
|
res.json({ message: `Update user ${req.params.id}` });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/api/users/:id', (req, res) => {
|
||||||
|
res.json({ message: `Delete user ${req.params.id}` });
|
||||||
|
});
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=10]{Route paraméterek}
|
||||||
|
\begin{exampleblock}{:id paraméter}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
app.get('/api/users/:id', (req, res) => {
|
||||||
|
const userId = req.params.id;
|
||||||
|
res.json({ message: `User ID: ${userId}` });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Több paraméter
|
||||||
|
app.get('/api/users/:userId/posts/:postId', (req, res) => {
|
||||||
|
const { userId, postId } = req.params;
|
||||||
|
res.json({ userId, postId });
|
||||||
|
});
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
|
||||||
|
\begin{alertblock}{req.params}
|
||||||
|
URL-ben megadott dinamikus értékek.
|
||||||
|
\end{alertblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=10]{Query paraméterek}
|
||||||
|
\begin{exampleblock}{Query string}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
// GET /api/users?page=1&limit=10&role=admin
|
||||||
|
|
||||||
|
app.get('/api/users', (req, res) => {
|
||||||
|
const { page, limit, role } = req.query;
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
page: page || 1,
|
||||||
|
limit: limit || 10,
|
||||||
|
role: role
|
||||||
|
});
|
||||||
|
});
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
|
||||||
|
\begin{alertblock}{req.query}
|
||||||
|
URL query string paraméterek (?key=value).
|
||||||
|
\end{alertblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=15]{Query - Szűrés, Rendezés, Lapozás}
|
||||||
|
\begin{exampleblock}{GET /api/users?page=1\&limit=10\&sort=name\&role=admin}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
app.get('/api/users', (req, res) => {
|
||||||
|
let { page = 1, limit = 10, sort = 'id', role } = req.query;
|
||||||
|
|
||||||
|
page = parseInt(page);
|
||||||
|
limit = parseInt(limit);
|
||||||
|
|
||||||
|
let filteredUsers = users;
|
||||||
|
if (role) {
|
||||||
|
filteredUsers = filteredUsers.filter(u => u.role === role);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rendezés
|
||||||
|
filteredUsers.sort((a, b) => a[sort] > b[sort] ? 1 : -1);
|
||||||
|
|
||||||
|
// Lapozás
|
||||||
|
const start = (page - 1) * limit;
|
||||||
|
const paginatedUsers = filteredUsers.slice(start, start + limit);
|
||||||
|
|
||||||
|
res.json({ data: paginatedUsers, total: filteredUsers.length });
|
||||||
|
});
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=15]{Express Router létrehozása}
|
||||||
|
\begin{exampleblock}{routes/users.js}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/', (req, res) => {
|
||||||
|
res.json({ message: 'Get all users' });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/:id', (req, res) => {
|
||||||
|
res.json({ message: `Get user ${req.params.id}` });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/', (req, res) => {
|
||||||
|
res.json({ message: 'Create user' });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put('/:id', (req, res) => {
|
||||||
|
res.json({ message: `Update user ${req.params.id}` });
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=10]{Router importálása}
|
||||||
|
\begin{exampleblock}{app.js}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
const express = require('express');
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
const userRoutes = require('./routes/users');
|
||||||
|
const productRoutes = require('./routes/products');
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
app.use('/api/users', userRoutes);
|
||||||
|
app.use('/api/products', productRoutes);
|
||||||
|
|
||||||
|
app.listen(3000);
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
|
||||||
|
\begin{alertblock}{Prefix}
|
||||||
|
Az \texttt{app.use('/api/users', userRoutes)} minden route-hoz hozzáadja a prefix-et.
|
||||||
|
\end{alertblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=15]{Nested (beágyazott) route-ok}
|
||||||
|
\begin{exampleblock}{Hierarchikus erőforrások}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
// GET /api/users/:userId/posts
|
||||||
|
router.get('/:userId/posts', (req, res) => {
|
||||||
|
const { userId } = req.params;
|
||||||
|
res.json({ message: `Posts for user ${userId}` });
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/users/:userId/posts/:postId
|
||||||
|
router.get('/:userId/posts/:postId', (req, res) => {
|
||||||
|
const { userId, postId } = req.params;
|
||||||
|
res.json({ message: `User ${userId}, Post ${postId}` });
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/users/:userId/posts
|
||||||
|
router.post('/:userId/posts', (req, res) => {
|
||||||
|
const { userId } = req.params;
|
||||||
|
res.json({ message: `Create post for user ${userId}` });
|
||||||
|
});
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=15]{Controller-ek használata (1/2)}
|
||||||
|
\begin{block}{Separation of Concerns}
|
||||||
|
Route-ok csak az útvonalat definiálják, Controller-ek tartalmazzák a logikát.
|
||||||
|
\end{block}
|
||||||
|
|
||||||
|
\begin{exampleblock}{controllers/user.controller.js}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
exports.getAllUsers = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const users = await User.find();
|
||||||
|
res.json(users);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.getUserById = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const user = await User.findById(req.params.id);
|
||||||
|
if (!user) return res.status(404).json({ error: 'Not found' });
|
||||||
|
res.json(user);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=10]{Controller-ek használata (2/2)}
|
||||||
|
\begin{exampleblock}{routes/users.js}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const userController = require('../controllers/user.controller');
|
||||||
|
|
||||||
|
router.get('/', userController.getAllUsers);
|
||||||
|
router.get('/:id', userController.getUserById);
|
||||||
|
router.post('/', userController.createUser);
|
||||||
|
router.put('/:id', userController.updateUser);
|
||||||
|
router.delete('/:id', userController.deleteUser);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
|
||||||
|
\begin{alertblock}{Tiszta kód}
|
||||||
|
Route fájlok rövidek, Controller-ekben a logika.
|
||||||
|
\end{alertblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=10]{Middleware route-okban}
|
||||||
|
\begin{exampleblock}{Auth middleware route-ra}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
const auth = require('../middlewares/auth');
|
||||||
|
|
||||||
|
router.get('/protected', auth, (req, res) => {
|
||||||
|
res.json({ message: 'Protected data' });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/users', auth, validateUser, userController.createUser);
|
||||||
|
|
||||||
|
// Minden route-ra
|
||||||
|
router.use('/admin', auth, adminRoutes);
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
|
||||||
|
\begin{alertblock}{Több middleware}
|
||||||
|
Több middleware függvényt is alkalmazhatsz sorban.
|
||||||
|
\end{alertblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=15]{API verziókezelés (1/2)}
|
||||||
|
\begin{exampleblock}{Verziókezelés URL-ben}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
// V1 routes
|
||||||
|
const v1Routes = require('./routes/v1');
|
||||||
|
app.use('/api/v1', v1Routes);
|
||||||
|
|
||||||
|
// V2 routes
|
||||||
|
const v2Routes = require('./routes/v2');
|
||||||
|
app.use('/api/v2', v2Routes);
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
|
||||||
|
\begin{exampleblock}{Struktúra}
|
||||||
|
\tiny
|
||||||
|
\begin{itemize}
|
||||||
|
\item routes/v1/index.js
|
||||||
|
\item routes/v1/users.js
|
||||||
|
\item routes/v1/products.js
|
||||||
|
\item routes/v2/index.js
|
||||||
|
\item routes/v2/users.js
|
||||||
|
\end{itemize}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=10]{API verziókezelés (2/2)}
|
||||||
|
\begin{exampleblock}{routes/v1/index.js}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const userRoutes = require('./users');
|
||||||
|
const productRoutes = require('./products');
|
||||||
|
|
||||||
|
router.use('/users', userRoutes);
|
||||||
|
router.use('/products', productRoutes);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
|
||||||
|
\begin{exampleblock}{Használat}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
GET /api/v1/users
|
||||||
|
GET /api/v2/users
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[shrink=10]{Route Best Practices}
|
||||||
|
\begin{alertblock}{Konvenciók}
|
||||||
|
\begin{enumerate}
|
||||||
|
\item Használj \textbf{főneveket}: \texttt{/users}, \texttt{/products}
|
||||||
|
\item Használj \textbf{többes számot}: \texttt{/users} $\checkmark$
|
||||||
|
\item \textbf{Hierarchia}: \texttt{/users/:userId/posts/:postId}
|
||||||
|
\item \textbf{Kisbetűs URL}: \texttt{/api/users} $\checkmark$
|
||||||
|
\item \textbf{Kötőjel}: \texttt{/user-profiles} $\checkmark$
|
||||||
|
\item \textbf{Verziókezelés}: \texttt{/api/v1/users}
|
||||||
|
\item \textbf{Szűrés query-vel}: \texttt{/users?role=admin}
|
||||||
|
\item \textbf{Ne igék}: \texttt{POST /users} $\checkmark$, \texttt{/createUser} $\times$
|
||||||
|
\end{enumerate}
|
||||||
|
\end{alertblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}{Összefoglalás - Routing}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Routing = kérések irányítása végpontokra
|
||||||
|
\item Route paraméterek: \texttt{/users/:id} $\rightarrow$ \texttt{req.params.id}
|
||||||
|
\item Query paraméterek: \texttt{/users?page=1} $\rightarrow$ \texttt{req.query.page}
|
||||||
|
\item Express Router: moduláris route szervezés
|
||||||
|
\item Controller-ek: logika szétválasztása
|
||||||
|
\item Middleware: route-specifikus middleware-ek
|
||||||
|
\item Verziókezelés: /api/v1, /api/v2
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
\vspace{0.5cm}
|
||||||
|
|
||||||
|
\end{frame}
|
||||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
|||||||
|
\documentclass[usenames,dvipsnames,aspectratio=169]{beamer}
|
||||||
|
\usepackage{../common/webfejl}
|
||||||
|
|
||||||
|
% Automatikus frame törés engedélyezése túl hosszú tartalomnál
|
||||||
|
\setbeamertemplate{frametitle continuation}[from second][\insertcontinuationcountroman]
|
||||||
|
|
||||||
|
\title[Webtechnológia és webalkalmazás-fejlesztés - Rest API]{Webtechnológia és webalkalmazás-fejlesztés - Rest API}
|
||||||
|
\subtitle{Rest API}
|
||||||
|
|
||||||
|
\begin{document}
|
||||||
|
|
||||||
|
\begin{frame}[plain]
|
||||||
|
\titlepage
|
||||||
|
\logoalul
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\input{restapi_alapok.tex}
|
||||||
|
\input{Architektura.tex}
|
||||||
|
\input{CQRS.tex}
|
||||||
|
\input{Express_restapi.tex}
|
||||||
|
\input{Routing.tex}
|
||||||
|
\input{Postman.tex}
|
||||||
|
|
||||||
|
\end{document}
|
||||||
@@ -0,0 +1,286 @@
|
|||||||
|
% REST API alapok
|
||||||
|
|
||||||
|
\section{REST API Alapok}
|
||||||
|
|
||||||
|
\begin{frame}{Mi a REST API?}
|
||||||
|
\begin{block}{REST - Representational State Transfer}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Architektúra stílus webes szolgáltatásokhoz
|
||||||
|
\item Roy Fielding dolgozta ki 2000-ben
|
||||||
|
\item HTTP protokollon alapuló kommunikáció
|
||||||
|
\item Állapotmentes (stateless) kliens-szerver kapcsolat
|
||||||
|
\end{itemize}
|
||||||
|
\end{block}
|
||||||
|
|
||||||
|
\begin{exampleblock}{API - Application Programming Interface}
|
||||||
|
Interfész, amin keresztül különböző alkalmazások kommunikálnak egymással.
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}{REST alapelvek}
|
||||||
|
\begin{enumerate}
|
||||||
|
\item \textbf{Kliens-szerver architektúra}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Szétválasztott felelősségek
|
||||||
|
\item Kliens: felhasználói felület
|
||||||
|
\item Szerver: adatkezelés, üzleti logika
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
\item \textbf{Állapotmentesség (Stateless)}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Minden kérés független
|
||||||
|
\item Szerver nem tárol kliens állapotot
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
\item \textbf{Cachelhető}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Válaszok cache-elhetők a teljesítmény növelésére
|
||||||
|
\end{itemize}
|
||||||
|
\end{enumerate}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}{REST alapelvek (folyt.)}
|
||||||
|
\begin{enumerate}
|
||||||
|
\setcounter{enumi}{3}
|
||||||
|
\item \textbf{Egységes interfész}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Erőforrások URI-kon keresztül azonosíthatók
|
||||||
|
\item Reprezentációkon keresztüli manipuláció
|
||||||
|
\item Önleíró üzenetek
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
\item \textbf{Rétegzett rendszer}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Köztes rétegek (proxy, gateway) használhatók
|
||||||
|
\item Kliens nem tudja, közvetlenül a szerverhez kapcsolódik-e
|
||||||
|
\end{itemize}
|
||||||
|
\end{enumerate}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}{Erőforrások és URI-k}
|
||||||
|
\begin{block}{Erőforrás (Resource)}
|
||||||
|
Minden, amit azonosítani és kezelni akarunk (pl. felhasználó, termék, rendelés).
|
||||||
|
\end{block}
|
||||||
|
|
||||||
|
\begin{exampleblock}{URI példák}
|
||||||
|
\begin{itemize}
|
||||||
|
\item \texttt{/users} - Összes felhasználó
|
||||||
|
\item \texttt{/users/123} - Adott felhasználó (ID: 123)
|
||||||
|
\item \texttt{/users/123/posts} - Egy felhasználó posztjai
|
||||||
|
\item \texttt{/products} - Összes termék
|
||||||
|
\item \texttt{/products/456} - Adott termék
|
||||||
|
\end{itemize}
|
||||||
|
\end{exampleblock}
|
||||||
|
|
||||||
|
\begin{alertblock}{Konvenció}
|
||||||
|
Használj \textbf{főneveket} és \textbf{többes számot}: \texttt{/users}, \texttt{/products}
|
||||||
|
\end{alertblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}{HTTP metódusok}
|
||||||
|
\begin{table}
|
||||||
|
\small
|
||||||
|
\begin{tabular}{|l|l|l|l|}
|
||||||
|
\hline
|
||||||
|
\textbf{Metódus} & \textbf{Művelet} & \textbf{Idemp.} & \textbf{Példa} \\
|
||||||
|
\hline
|
||||||
|
GET & Lekérdezés & Igen & Lista/részlet \\
|
||||||
|
POST & Létrehozás & Nem & Új elem \\
|
||||||
|
PUT & Frissítés (teljes) & Igen & Teljes csere \\
|
||||||
|
PATCH & Frissítés (rész) & Nem & Rész módosítás \\
|
||||||
|
DELETE & Törlés & Igen & Elem törlése \\
|
||||||
|
\hline
|
||||||
|
\end{tabular}
|
||||||
|
\end{table}
|
||||||
|
|
||||||
|
\vspace{0.3cm}
|
||||||
|
|
||||||
|
\begin{block}{Idempotencia}
|
||||||
|
Ugyanaz a kérés többször végrehajtva ugyanazt az eredményt adja.
|
||||||
|
\end{block}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=10]{HTTP metódusok - GET példák}
|
||||||
|
\begin{exampleblock}{Összes elem lekérdezése}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
GET /api/users
|
||||||
|
Response: 200 OK
|
||||||
|
[
|
||||||
|
{"id": 1, "name": "Alice", "email": "alice@example.com"},
|
||||||
|
{"id": 2, "name": "Bob", "email": "bob@example.com"}
|
||||||
|
]
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
|
||||||
|
\begin{exampleblock}{Egy elem lekérdezése}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
GET /api/users/1
|
||||||
|
Response: 200 OK
|
||||||
|
{"id": 1, "name": "Alice", "email": "alice@example.com"}
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=10]{HTTP metódusok - POST, PUT}
|
||||||
|
\begin{exampleblock}{POST - Új elem létrehozása}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
POST /api/users
|
||||||
|
Body: {"name": "Charlie", "email": "charlie@example.com"}
|
||||||
|
Response: 201 Created
|
||||||
|
{"id": 3, "name": "Charlie", "email": "charlie@example.com"}
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
|
||||||
|
\begin{exampleblock}{PUT - Teljes frissítés}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
PUT /api/users/1
|
||||||
|
Body: {"name": "Alice Smith", "email": "alice.smith@example.com"}
|
||||||
|
Response: 200 OK
|
||||||
|
{"id": 1, "name": "Alice Smith", "email": "alice.smith@example.com"}
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=10]{HTTP metódusok - PATCH, DELETE}
|
||||||
|
\begin{exampleblock}{PATCH - Részleges frissítés}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
PATCH /api/users/1
|
||||||
|
Body: {"email": "newemail@example.com"}
|
||||||
|
Response: 200 OK
|
||||||
|
{"id": 1, "name": "Alice", "email": "newemail@example.com"}
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
|
||||||
|
\begin{exampleblock}{DELETE - Törlés}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
DELETE /api/users/1
|
||||||
|
Response: 204 No Content
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}{HTTP státuszkódok}
|
||||||
|
\begin{columns}
|
||||||
|
\begin{column}{0.48\textwidth}
|
||||||
|
\begin{block}{2xx - Sikeres}
|
||||||
|
\begin{itemize}
|
||||||
|
\item \textbf{200} OK
|
||||||
|
\item \textbf{201} Created
|
||||||
|
\item \textbf{204} No Content
|
||||||
|
\end{itemize}
|
||||||
|
\end{block}
|
||||||
|
|
||||||
|
\begin{block}{4xx - Kliens hiba}
|
||||||
|
\begin{itemize}
|
||||||
|
\item \textbf{400} Bad Request
|
||||||
|
\item \textbf{401} Unauthorized
|
||||||
|
\item \textbf{403} Forbidden
|
||||||
|
\item \textbf{404} Not Found
|
||||||
|
\end{itemize}
|
||||||
|
\end{block}
|
||||||
|
\end{column}
|
||||||
|
|
||||||
|
\begin{column}{0.48\textwidth}
|
||||||
|
\begin{block}{5xx - Szerver hiba}
|
||||||
|
\begin{itemize}
|
||||||
|
\item \textbf{500} Internal Error
|
||||||
|
\item \textbf{502} Bad Gateway
|
||||||
|
\item \textbf{503} Service Unavailable
|
||||||
|
\end{itemize}
|
||||||
|
\end{block}
|
||||||
|
|
||||||
|
\begin{alertblock}{Fontos}
|
||||||
|
Mindig a megfelelő státuszkódot küldd!
|
||||||
|
\end{alertblock}
|
||||||
|
\end{column}
|
||||||
|
\end{columns}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=10]{JSON formátum}
|
||||||
|
\begin{block}{JavaScript Object Notation}
|
||||||
|
A REST API-k leggyakoribb adatformátuma.
|
||||||
|
\end{block}
|
||||||
|
|
||||||
|
\begin{exampleblock}{Példa}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "John Doe",
|
||||||
|
"email": "john@example.com",
|
||||||
|
"age": 30,
|
||||||
|
"active": true,
|
||||||
|
"roles": ["user", "admin"],
|
||||||
|
"address": {
|
||||||
|
"city": "Budapest",
|
||||||
|
"zip": "1111"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}{REST vs SOAP}
|
||||||
|
\begin{table}
|
||||||
|
\tiny
|
||||||
|
\begin{tabular}{|l|p{4cm}|p{4cm}|}
|
||||||
|
\hline
|
||||||
|
& \textbf{REST} & \textbf{SOAP} \\
|
||||||
|
\hline
|
||||||
|
Protokoll & HTTP & HTTP, SMTP, TCP \\
|
||||||
|
Formátum & JSON, XML & Csak XML \\
|
||||||
|
Állapot & Stateless & Lehet stateful \\
|
||||||
|
Sebesség & Gyorsabb & Lassabb \\
|
||||||
|
Használat & Egyszerűbb & Komplexebb \\
|
||||||
|
\hline
|
||||||
|
\end{tabular}
|
||||||
|
\end{table}
|
||||||
|
|
||||||
|
\vspace{0.3cm}
|
||||||
|
|
||||||
|
\begin{exampleblock}{Mikor REST?}
|
||||||
|
Webes és mobil alkalmazások, publikus API-k, mikroszolgáltatások
|
||||||
|
\end{exampleblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile,shrink=10]{Hibakezelés}
|
||||||
|
\begin{exampleblock}{Hibaválasz struktúra}
|
||||||
|
\tiny
|
||||||
|
\begin{verbatim}
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"code": "USER_NOT_FOUND",
|
||||||
|
"message": "A megadott felhasználó nem található",
|
||||||
|
"details": "User with ID 123 does not exist",
|
||||||
|
"timestamp": "2026-01-27T20:30:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
\end{verbatim}
|
||||||
|
\end{exampleblock}
|
||||||
|
|
||||||
|
\begin{alertblock}{Best Practices}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Mindig küldd a megfelelő HTTP státuszkódot
|
||||||
|
\item Adj értelmes hibaüzeneteket
|
||||||
|
\item Ne adj ki érzékeny információkat
|
||||||
|
\end{itemize}
|
||||||
|
\end{alertblock}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}{Összefoglalás - REST Alapok}
|
||||||
|
\begin{itemize}
|
||||||
|
\item \textbf{REST} = Representational State Transfer (Reprezentációs Állapotátvitel)
|
||||||
|
\item Architektúra stílus HTTP-n keresztül
|
||||||
|
\item \textbf{Erőforrások} (Resources) URI-kon keresztül azonosíthatók
|
||||||
|
\item \textbf{HTTP metódusok}: GET (olvasás), POST (létrehozás), PUT/PATCH (módosítás), DELETE (törlés)
|
||||||
|
\item \textbf{Státuszkódok} (Status Codes) jelzik a kérés eredményét
|
||||||
|
\item \textbf{JSON} a leggyakoribb adatformátum
|
||||||
|
\item \textbf{Állapotmentes} (Stateless) kommunikáció
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
@@ -3,6 +3,12 @@
|
|||||||
\usepackage[magyar]{babel}
|
\usepackage[magyar]{babel}
|
||||||
\usepackage{indentfirst}
|
\usepackage{indentfirst}
|
||||||
\usepackage{graphicx}
|
\usepackage{graphicx}
|
||||||
|
\usepackage{tikz}
|
||||||
|
\usetikzlibrary{positioning}
|
||||||
|
\usepackage{fancyvrb}
|
||||||
|
|
||||||
|
% Globális verbatim beállítás - tiny betűméret minden verbatim blokkhoz
|
||||||
|
\fvset{fontsize=\tiny}
|
||||||
\usepackage{listingsutf8}
|
\usepackage{listingsutf8}
|
||||||
\usepackage{textcomp}
|
\usepackage{textcomp}
|
||||||
\usepackage{eurosym}
|
\usepackage{eurosym}
|
||||||
|
|||||||
Reference in New Issue
Block a user