express ist eine Library für Backend Entwicklung
im Grunde genommen kann es HTTP Requests entgegennehmen und zugehörige Responses versenden
typische Anwendungsfälle: Entwicklung einer Website (Responses sind HTML), Entwicklung eines APIs (Responses sind JSON oder andere Datenformate)
Beispiele:
Bekommt einen Request zu API-Daten (z.B. /api/article/19e2), sendet Daten zurück
Bekommt einen Request zu einer statischen Ressource, sendet diese Ressource zurück (z.B. styles.css, logo.png, ...)
Bekommt einen Request zu einer Seite (z.B. /news), rendert die Seite als HTML und sendet sie zurück
die meisten Projekte verwenden express
hello world mit express:
// server.js
import express from 'express';
const app = express();
// provide a function that handles a get request to "/"
// and sends a response
app.get('/', (req, res) => {
// note: we should actually return a complete HTML file
res.send('<h1>Hello World!</h1>\n');
});
// listen on localhost:3000
app.listen(3000);
hello world ohne express (siehe https://nodejs.org/en/docs/guides/getting-started-guide/):
// server.js
import http from 'http';
const handler = (req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html; charset=UTF-8');
res.end('<h1>Hello World!</h1>\n');
};
const server = http.createServer(handler);
server.listen(3000);
einfaches start Skript um den Server zu starten:
{
"scripts": {
"start": "node server.js"
}
}
Um den Server zu starten und bei Code-Änderungen neu zu starten:
Installiere nodemon von npm und füge ein dev-Skript zu package.json hinzu:
{
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
}
}
Web-Entwicklung in node geschieht über Request-Handler-Funktionen, z.B.:
(req, res) => {
res.send('<h1>Hello World!</h1>\n');
};
Ein Request Handler bekommt zwei Argumente übergeben:
req
- repräsentiert den eingehenden requestres
- repräsentiert die response / Antwort, die wir senden werdenBeispiel für ein Request-Objekt:
{
"method": "GET",
"path": "/products/123",
"params": { "id": "123" },
"headers": { "user-agent": "Mozilla/5.0 (Windows ..." }
}
Klasse in reinem Node: IncomingMessage
Unterklasse in express: Request
Klasse in reinem Node: ServerResponse
Unterklasse in express: Response
antworte für /hello mit einer Hello World-Nachricht:
app.get('/hello', (req, res) => {
res.send('<h1>Hello World!</h1>\n');
});
Sende eine statische Datei:
app.get('/favicon.ico', (req, res) => {
res.sendFile('public/favicon.ico');
});
Lade Datenbankeinträge und sende sie als JSON:
app.get('/api/products', async (req, res) => {
// use a DB library for accessing the database
const sqlQuery = 'SELECT * FROM product';
const [results] = await db.query(sqlQuery);
// send the results as JSON
res.json(results);
});
Lade einen Datenbankeintrag basierend auf einem URL-Parameter und sende ihne als JSON:
app.get('/api/products/:id', async (req, res) => {
const id = Number(req.params.id);
const sqlQuery = 'SELECT * FROM product WHERE id = ?';
const [results] = await db.query(sqlQuery, [id]);
res.json(results[0]);
});
Lade Datenbankeinträge basierend auf Query Parametern
Beispiel für eine angefragte URL: /api/products?min_rating=4&max_price=200
app.get('/api/products', async (req, res) => {
const minRating = Number(req.query.min_rating);
const maxPrice = Number(req.query.max_price);
const sqlQuery =
'SELECT * FROM product WHERE rating >= ? AND price <= ?';
const sqlParams = [minRating, maxPrice];
const [results] = await db.query(sqlQuery, sqlParams);
res.json(results);
});
Rendere eine HTML-Seite basierend auf einem Datenbankeintrag
app.get('/products/:id', async (req, res) => {
const sqlQuery = 'SELECT * FROM product WHERE id = ?';
const sqlParams = [req.params.id];
const [results] = await db.query(sqlQuery, sqlParams);
// look for the "product_detail" template and render it based on
// the retrieved data
res.render('product_detail', {
name: results[0].name,
price: results[0].price,
});
});
app.get('/', (req, res) => {
res.send('<h1>Home</h1>\n');
});
app.get('/about', (req, res) => {
res.send('<h1>About</h1>\n');
});
andere HTTP-Methoden werden behandelt via: .post
, .put
, .delete
, ...
Methoden von Response-Objekten:
.status()
- um den Statuscode zu setzen.set()
- um Header zu setzen.send()
- um Inhalte zu sendenexplizites Beispiel:
res.set({ 'Content-Type': 'text/html' });
res.status(200);
res.send('<!DOCTYPE html><html>...</html>');
standardmäßig wird der Content-Type auf text/html und der Status auf 200 gesetzt
kürzere Version:
res.send('<!DOCTYPE html><html>...</html>');
weiteres Beispiel:
res.status(404);
res.set({ 'Content-Type': 'text/plain' });
res.send('Document not found.\n');
Response-Objekte unterstützen Method Chaining:
res
.status(404)
.set({ 'Content-Type': 'text/plain' })
.send('Document not found.\n');
Beispiele für URLs mit Parametern:
Beispiel-URL: /products/123 (verweist über eine ID auf ein einzelnes Produkt)
app.get('/products/:id', (req, res) => {
const productId = Number(req.params.id);
// ...
});
Beispiel-URL: /products?min_rating=4&max_price=200 (filtert anhand von Kriterien)
app.get('/products', (req, res) => {
const minRating = Number(req.query.min_rating);
const maxPrice = Number(req.query.max_price);
// ...
});
Middleware kann auf Requests reagieren sowie die req / res - Objekte abändern
Beispiel:
import compression from 'compression';
import express from 'express';
import morgan from 'morgan';
const app = express();
// log all incoming requests
app.use(morgan('common'));
// serve content of the "public" folder as-is if found
app.use(express.static('public'));
// compress all responses
app.use(compression());
app.get('/', (req, res) => {
// ...
});
// log all incoming requests
app.use(morgan('common'));
// for any request that starts with /api, parse its
// JSON body if it exists
app.use('/api', express.json());
// compress API responses
app.use('/api', compression());
// requests to /assets/...: send back static files
app.use('/assets', express.static('public'));
req.body
zur Verfügungreq.cookies
zur Verfügungreq.session
)senden von JSON (manuell):
res.setHeader({"Content-Type", "application/json"});
res.send(JSON.stringify(data));
mit dem .json()
-Helper:
res.json(data);
Empfangen (parsen) eines JSON-Bodys via middleware:
app.use('/api', express.json());
Daten sind dann verfügbar als req.body
Same-origin Policy: Sicherheitsregel im Browser:
Standardmäßig darf eine Seite von einer Website (z.B. www.example.com) keine Requests an eine andere Website (z.B. api.example.com) schicken
Beispiel: Gehe auf eine Website (z.B. Wikipedia) öffne die JavaScript-Konsole des Browsers und sende einen request zu einer anderen Website, z.B. via fetch("https://google.com")
Resultat: der Request wird verhindert
Grund: um das Ausnützen von cookie-basierter Authentifizierung durch Angreifer zu verhindern
Die angefragte Website kann Cross-Origin resource sharing (CORS) für manche URLs oder für alle URLs erlauben
Das geschieht mit dem HTTP-Header "Access-Control-Allow-Origin"
Beispiel: die jsonplaceholder API erlaubt CORS für alle Websites - also funktioniert fetch("https://jsonplaceholder.typicode.com/todos")
von jeder Seite
CORS in express aktivieren:
import cors from 'cors';
Erlauben von Requests von allen Domains:
app.use('/api', cors());
Erlauben von Requests von einer bestimmten Domain:
app.use('/api', cors({ origin: 'https://example.com' }));
Wir erstellen eine Rest API mit express und einer Datenbank, die mittels der mysql2 Library verbunden ist.
Die Datenbank enthält eine einzelne Tabelle namens "task"
Bemerkung: Verschiedene Rest APIs können sich bei Mutationen oft unterschiedlich verhalten
Beispiel: Mögliche Antworten, wenn ein Eintrag mittels POST erstellt wurde:
grundlegendes Setup:
app.use('/api', express.json());
app.use('/api', cors());
Alle Einträge einer Ressource abfragen:
app.get('/api/tasks', async (req, res) => {
const [results] = await db.query('SELECT * FROM task;');
res.json(results);
});
nach einer bestimmten id abfragen:
app.get('/api/tasks/:id', async (req, res) => {
const id = Number(req.params.id);
const sqlStatement = 'SELECT * FROM task WHERE id = ?';
const [results] = await db.query(sqlStatement, [id]);
if (results.length === 0) {
// 404 Not Found
res.status(404).send();
} else {
res.json(results[0]);
}
});
nach bestimmten Parametern abfragen (is_done, due_date):
app.get('/api/tasks/', async (req, res) => {
// array of conditions - e.g. ['is_done = ?', 'due_date = ?']
const conditions = [];
// array of corresponding values - e.g. [1, '2020-10-30']
const values = [];
if (req.query.due_date !== undefined) {
conditions.push('due_date = ?');
values.push(req.query.due_date);
}
if (req.query.is_done !== undefined) {
conditions.push('is_done = ?');
// can be either "0" or "1"
values.push(Number(req.query.is_done));
}
let sqlStatement = 'SELECT * FROM task';
if (conditions.length > 0) {
sqlStatement += ` WHERE ${conditions.join(' AND ')}`;
}
// sqlStatement = complete SQL statement with variables, e.g.:
// SELECT * FROM task WHERE is_done = ? AND due_date = ?
// values = corresponding values, e.g.:
// [1, '2020-10-30']
const [results] = await db.query(sqlStatement, values);
res.json(results);
});
Einen neuen Eintrag erstellen:
app.post('/api/tasks', async (req, res) => {
const isDone = req.body.is_done;
const dueDate = req.body.due_date;
if (isDone === undefined || dueDate === undefined) {
// 400 Bad Request
res.status(400).send();
return;
}
const sqlStatement = `INSERT INTO task (due_date, is_done) VALUES (?, ?)`;
const values = [dueDate, isDone];
const [results] = await db.query(sqlStatement, values);
// 201 Created (or 200 OK)
res.status(201).json({ id: results.insertId });
});
einen Eintrag ersetzen - via put:
app.put('/api/tasks/:id', async (req, res) => {
const isDone = req.body.is_done;
const dueDate = req.body.due_date;
if (isDone === undefined || dueDate === undefined) {
// 400 Bad Request
res.status(400).send();
return;
}
const id = Number(req.params.id);
const sqlStatement = `
UPDATE task
SET due_date = ?, is_done = ?
WHERE id = ?
`;
const values = [dueDate, isDone, id];
const [results] = await db.query(sqlStatement, values);
if (results.affectedRows === 0) {
// 404 Not Found
res.status(404).send();
} else {
res.send();
}
});
Löschen eines Eintrags:
app.delete('/api/tasks/:id', async (req, res) => {
const id = Number(req.params.id);
const sqlStatement = `DELETE FROM task WHERE id = ?`;
const values = [id];
const [results] = await db.query(sqlStatement, values);
if (results.affectedRows === 0) {
// 404 Not Found
res.status(404).send();
} else {
res.send();
}
});
APIs sind nicht so einfach zu testen wie Scripts oder UIs
Möglichkeiten, ein API zu testen:
Einfacher Test ohne zusätzliche Tools:
async function testGetTodos() {
console.log('/todos: returns an array of 200 todos');
const res = await fetch(
'https://jsonplaceholder.typicode.com/todos'
);
const data = await res.json();
if (data.length !== 200) {
console.log('FAILED');
}
}
await testGetTodos();
Test mit nodes eingebauten Test-Tools:
import { test } from 'node:test';
import assert from 'node:assert';
test('/todos: returns an array of 200 todos', async () => {
const res = await fetch(
'https://jsonplaceholder.typicode.com/todos'
);
const data = await res.json();
assert.strictEqual(data.length, 200);
});
senden von nur einer statischen Datei:
app.get('/favicon.ico', (req, res) => {
res.sendFile('public/favicon.ico');
});
mit Middleware kann ein Ordner für statische Dateien angegeben werden:
Um für jeden Request nach statischen Inhalten zu suchen:
app.use(express.static('public'));
Um statische Inhalte nur für Requests nach /static zu durchsuchen:
app.use('/static', express.static('public'));
express kann HTML mit dynamischen Inhalten generieren - das HTML wird für jeden Request neu generiert (gerendert)
manuell (kann angreifbar sein):
app.get('/', (req, res) => {
const name = 'world';
res.send(`
<!DOCTYPE html>
<html>
<head><title>Hello ${name}</title></head>
<body>Hello ${name}</body>
</html>
`);
});
mit Hilfe einer Template Engine:
allgemeines Verfahren:
import express from 'express';
import myengine from 'myengine';
const app = express();
// specify the rendering engine
app.set('view engine', 'myengine');
app.get('/', (req, res) => {
const name = 'world';
// renders 'views/index.myengine'
res.render('index', { name: name });
});
Beispiel mit handlebars:
npm install hbs
app.set('view engine', 'hbs');
// ...
app.get('/', (req, res) => {
const name = 'world';
// renders the file 'views/index.hbs'
res.render('index', { name: name });
});
(ersetze hbs durch ejs / pug für andere Template-Enginges)
Übungen:
Pokeapi Teil 1:
app.get('/pokemon/:id', async (req, res) => {
const id = req.params.id;
const dataRes = await fetch(
`https://pokeapi.co/api/v2/pokemon/${id}`
);
const data = await dataRes.json();
res.render('pokemon', {
id: id,
name: data.name,
imgSrc: data.sprites.front_default,
});
});
app.get('/pokemon', (req, res) => {
res.redirect('/pokemon/1');
});
Pokeapi Teil 2:
views/pokemon.hbs:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>{{name}}</title>
</head>
<body>
<h1>{{name}}</h1>
<img src="{{imgSrc}}" />
</body>
</html>
Standardmäßig sendet der Browser Formular-Inhalte im URL-encoded Format, z.B.:
foo=1&bar=2&baz=3
in get-Requests: als Teil der URL, z.B. https://google.com/search?ei=xyzg&q=foo...
in post-Requests: im Request-Body
in einem get-Request: lies req.query
in einem post-Request: verwende express.urlencoded
als Middleware, lies req.body