express is a library for backend development
fundamentally, it can receive HTTP requests and send corresponding responses
typical use cases: developing a website (responses are HTML), developing an API (responses are JSON or another data format)
examples:
receives a request for some API data (e.g. /api/article/19e2), sends back that data
receives a request for a static resource, sends back that resource (e.g. styles.css, logo.png, ...)
receives a request for a page (e.g. /news), renders and sends back that page as HTML
most projects will use express
hello world with 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 without express (see 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);
basic start script to start the server:
{
  "scripts": {
    "start": "node server.js"
  }
}
to start a server and restart it every time some code changes:
install nodemon from npm and specify a dev script in package.json:
{
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js"
  }
}
web development in node is based on request handler functions, e.g.:
(req, res) => {
  res.send('<h1>Hello World!</h1>\n');
};
a request handler function receives two arguments:
req - represents the incoming requestres - represents the response that we will sendexample for a request object:
{
  "method": "GET",
  "path": "/products/123",
  "params": { "id": "123" },
  "headers": { "user-agent": "Mozilla/5.0 (Windows ..." }
}
class in plain node: IncomingMessage
class in express: Request
class in plain node / connect: ServerResponse
subclass in express: Response
send a hello world message under /hello:
app.get('/hello', (req, res) => {
  res.send('<h1>Hello World!</h1>\n');
});
send a static file:
app.get('/favicon.ico', (req, res) => {
  res.sendFile('public/favicon.ico');
});
get database entries and send them back as 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);
});
get a database entry and send it as JSON based on a URL parameter:
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]);
});
get a database entry and send it as JSON based on query parameters
example request 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);
});
render some HTML based on a database entry
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');
});
other HTTP methods are handled via: .post, .put, .delete, ...
methods of response objects:
.status() - for setting the status code.set() - for setting headers.send() - for sending contentexplicit example:
res.set({ 'Content-Type': 'text/html' });
res.status(200);
res.send('<!DOCTYPE html><html>...</html>');
by default, the Content-Type will be text/html and the status will be 200
shorter version:
res.send('<!DOCTYPE html><html>...</html>');
another example:
res.status(404);
res.set({ 'Content-Type': 'text/plain' });
res.send('Document not found.\n');
response objects support method chaining:
res
  .status(404)
  .set({ 'Content-Type': 'text/plain' })
  .send('Document not found.\n');
examples of URLs with parameters:
example URL: /products/123 (references a specific product by an ID)
app.get('/products/:id', (req, res) => {
  const productId = req.params.id;
  // ...
});
example URL: /products?min_rating=4&max_price=200 (filters by some criteria)
app.get('/products', (req, res) => {
  const minRating = Number(req.query.min_rating);
  const maxPrice = Number(req.query.max_price);
  // ...
});
middleware can respond to requests or modify reqest / response objects
example use:
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.bodyreq.cookiesreq.session)sending JSON (manually):
res.setHeader({"Content-Type", "application/json"});
res.send(JSON.stringify(data));
with the .json() helper:
res.json(data);
receiving (parsing) a JSON body via middleware:
app.use("/api", express.json());
data will be available as req.body
Same-origin plicy: safety rule in the browser
by default, a webpage on one site (e.g. www.example.com) is not allowed to make requests to another site (e.g. api.example.com)
example: go to one website (e.g. Wikipedia), open the browser's JavaScript console and request another website, e.g. via fetch("https://google.com")
result: the request is prohibited
reason: to prevent exploitation of cookie-based authentication
The requested site may allow Cross-Origin resource sharing (CORS) for some URLs or for all URLs
This is done via the HTTP header "Access-Control-Allow-Origin"
example: the jsonplaceholder API enables CORS for all sites - so fetch("https://jsonplaceholder.typicode.com/todos") works from any site
enabling CORS in express:
import cors from 'cors';
allowing requests from all domains:
app.use('/api', cors());
allowing requests from a specific domain:
app.use('/api', cors({ origin: 'https://example.com' }));
We'll create an example Rest API with express and a database that's connected via the mysql2 library.
We will create a database named "tasklist" with a single table named "task"
note: Different Rest APIs can behave differently on mutations
example: possible responses when an entry has been created via POST:
basic setup:
app.use('/api', express.json());
app.use('/api', cors());
get all entries from a resource:
app.get('/api/tasks', async (req, res) => {
  const [results] = await db.query('SELECT * FROM task;');
  res.json(results);
});
get by a specific id:
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]);
  }
});
get by some query parameters (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);
});
creating a new entry:
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 });
});
replacing an entry - 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();
  }
});
delete an entry:
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 are not as strightforward to test as scripts or UIs
ways to test an API:
simple test without any extra 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 with node's built-in testing 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);
});
sending just a single static file:
app.get('/favicon.ico', (req, res) => {
  res.sendFile('public/favicon.ico');
});
specifying a folder that contains static files via middleware:
look for static content for every request:
app.use(express.static('public'));
look for static content only if the URL starts with /static:
app.use('/static', express.static('public'));
express can generate HTML with dynamic content - the HTML will be generated (rendered) for every request
manually (can be open to attacks):
app.get('/', (req, res) => {
  const name = 'world';
  res.send(`
    <!DOCTYPE html>
    <html>
      <head><title>Hello ${name}</title></head>
      <body>Hello ${name}</body>
    </html>
  `);
});
via a template engine:
general procedure:
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 });
});
example with 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 });
});
(replace hbs with ejs / pug for other template engines)
exercises:
Pokeapi part 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 part 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>
By default, the browser sends form contents in URL-encoded format, e.g.:
foo=1&bar=2&baz=3
in get requests: as part of the URL, e.g. https://google.com/search?ei=xyzg&q=foo...
in post requests: in the request body
in a get request: read req.query
in a post request: use express.urlencoded middleware, read req.body