Web Programming
Lecture 10

Structuring backend applications

Josue Obregon

Seoul National University of Science and Technology
Information Technology Management
Lecture slides index

May 8, 2026

Agenda

  • Single-file backends and their limits
  • Separation of concerns and MVC
  • Routes, controllers, and models
  • Request parameters and responses
  • Middleware
  • Project structure
  • API documentation with Swagger
  • Practical: refactoring the Flights API

Course structure

Roadmaps: Frontend, backend and fullstack

From simple APIs to structured backends

  • Typical structure of an early-stage Express application:
    • All routes defined in a single app.js
    • Request handling and business logic written inline
    • Data stored and accessed within route callbacks
  • Mixed responsibilities in one place:
    • Routing (URL handling)
    • Processing logic (validation, computation)
    • Data management (storage, retrieval)
  • Consequences as the project grows:
    • Reduced readability when the file becomes large
    • Difficult to isolate and debug issues
    • Limited reusability of logic across endpoints
    • Hard to extend without affecting unrelated parts

Why structure matters

  • Readability:
    • Clear organization of files and responsibilities
    • Faster understanding of system components
  • Maintainability:
    • Changes localized to specific parts of the system
    • Reduced risk of unintended side effects
  • Scalability:
    • Easier to introduce new features and endpoints
    • Supports growth of application complexity
  • Collaboration:
    • Enables parallel development across components
    • Establishes a predictable structure for team environments

Separation of concerns

  • Principle of dividing a system into distinct components:
    • Each component handles a specific responsibility
    • Minimizes overlap between functionalities
  • In backend applications, three primary concerns:
    • Routing: maps incoming requests to handlers
    • Logic: processes input, applies rules, prepares output
    • Data handling: manages storage and retrieval
  • Benefits:
    • Improved modularity
    • Easier testing and debugging
    • Greater code reusability

MVC pattern

  • Architectural pattern that organizes an application around three roles:

  • Model:

    • Represents data and domain logic
    • Defines how data is structured and accessed
  • View:

    • Represents the output presented to the client
    • In APIs, typically a structured JSON response
  • Controller:

    • Handles incoming requests
    • Coordinates between model and response generation
  • Responsibility boundaries:

    • Controllers do not manage data storage directly
    • Models do not handle HTTP-specific logic

MVC in Express applications

  • Express does not enforce a specific architecture:
    • No built-in MVC structure
    • Organization is defined by the developer
  • Common mapping in Express:
    • Routes → define endpoints and HTTP methods
    • Controllers → implement request-handling logic
    • Models → represent and manage data
  • Outcome:
    • MVC principles applied through folder and file organization
    • Flexible but requires explicit design decisions

Routes, controllers, models

  • Routes:
    • Define endpoint paths and HTTP methods
    • Map incoming requests to specific handlers
  • Controllers:
    • Contain request-handling logic
    • Process input, apply rules, generate responses
  • Models:
    • Represent application data
    • Provide structure for storing and retrieving information
  • Resulting separation:
    • Routes handle mapping
    • Controllers handle logic
    • Models handle data

Routes, controllers, models diagram

Request flow in a structured backend

Typical execution flow of an incoming request:

  • Client request:
    • HTTP request sent to backend (e.g., GET /flights)
  • Routing layer:
    • Matches the request to a defined endpoint
    • Forwards control to the corresponding controller
  • Controller layer:
    • Extracts parameters from the request
    • Applies processing logic and validation
  • Data layer (model):
    • Retrieves or modifies application data
  • Response:
    • Structured output returned to the client

Request parameters: unified view

  • Path parameters:
    • Defined as part of the URL structure
    • Used to identify specific resources
    • Example: /flights/:id
  • Query parameters:
    • Appended to the URL after ?
    • Used for optional filtering or configuration
    • Example: /flights?origin=Seoul
  • Body parameters:
    • Sent in the request payload
    • Typically used with POST and PUT requests
    • Contain structured data for creation or updates
  • Distinction:
    • Path → required resource identification
    • Query → optional modifiers
    • Body → data input for processing

Path parameters in Express

  • Path parameters act as placeholders in routes
  • Defined with :param and accessed through req.params
app.get("/flights/:id", function (req, res) {
  res.send("Flight ID: " + req.params.id);
});
  • Example request: GET /flights/42
  • Resulting req.params: { "id": "42" }
  • Path parameters always arrive as strings; convert to number when needed

Query parameters in Express

  • Query parameters are appended after ? in the URL
  • Accessed through req.query
  • Useful for optional values and filters
app.get("/flights", function (req, res) {
  let origin = req.query.origin;
  let destination = req.query.destination;
});
  • Example request: GET /flights?origin=Seoul&destination=Tokyo
  • Resulting req.query: { "origin": "Seoul", "destination": "Tokyo" }
  • Presence of each key is not guaranteed; validate before use

Body parameters in Express

  • Body parameters are sent inside the request payload
  • Used by POST, PUT, and PATCH requests to send structured data
  • Accessed through req.body
  • Requires JSON-parsing middleware: app.use(express.json())
app.post("/flights", function (req, res) {
  const { origin, destination, date } = req.body;
  if (!origin || !destination || !date) {
    return res.status(400).json({ error: "Missing required fields" });
  }
  // create the flight...
});
  • Always validate that required fields exist before using them

Response handling in Express

  • Express provides response methods to construct outgoing data:
Method Purpose
res.send() Sends a response body (text or data)
res.json() Sends a JSON response and sets the content type
res.status(code) Sets the HTTP status code
  • Methods can be chained:
res.status(200).json(data);
res.status(404).json({ error: "Not found" });

Error handling and status codes

Status codes communicate the outcome of request processing

  • Client errors (4xx):
    • 400 — invalid input or missing parameters
    • 404 — resource not found
    • 401 / 403 — authentication or authorization failures
  • Server errors (5xx):
    • 500 — unexpected failure in application logic
    • System or dependency-level issues
  • Practice:
    • Always return a meaningful JSON error message
    • Avoid silent failures or ambiguous responses

Returning errors

  • Combine res.status() with a JSON error body
  • Return early to avoid sending multiple responses
app.get("/flights/:id", function (req, res) {
  const id = Number(req.params.id);
  const flight = flights.find(f => f.id === id);

  if (!flight) {
    return res.status(404).json({ error: "Flight not found" });
  }

  res.status(200).json(flight);
});

Middleware concept

  • Middleware:
    • Functions executed during the request–response cycle
    • Receive req, res, and next as parameters
  • Role:
    • Process or transform request data before reaching controllers
    • Modify the response before it is sent
  • Execution:
    • Applied in the order they are registered
    • Each middleware passes control by calling next()
    • Or terminates the cycle by sending a response
  • Placement:
    • Globally with app.use(...)
    • At route level for selective application

Middleware pipeline

  • Request processing as a sequence of steps:
    • The incoming request passes through multiple middleware functions
    • Each step can inspect, modify, or terminate the request
  • Flow:
    • Request → middleware → route → controller → response
  • Central mechanism for extending application behavior

Common middleware in Express

  • JSON parsing:
    • express.json()
    • Converts JSON request bodies into req.body
  • URL-encoded parsing:
    • express.urlencoded({ extended: true })
    • Parses HTML form submissions
  • Logging:
    • Records incoming requests and responses
    • Useful for debugging and monitoring
  • Error-handling middleware:
    • Special signature with four arguments: (err, req, res, next)
    • Centralizes error responses
  • Future use cases:
    • Authentication
    • Input validation

Backend project structure

  • Standard organization for a structured Express backend
  • Each folder corresponds to a specific responsibility
  • Predictable layout that scales with the application
my-backend/
  app.js
  package.json
  routes/
    flights.js
  controllers/
    flightsController.js
  models/
    flightsModel.js
  • Responsibilities:
    • app.js — application configuration and initialization
    • routes/ — endpoint definitions
    • controllers/ — request-handling logic
    • models/ — data representation and access

Why API documentation matters

  • Purpose:
    • Describe how clients interact with the API
    • Define available endpoints, inputs, and outputs
  • Benefits:
    • Facilitates frontend–backend integration
    • Enables independent testing of endpoints
    • Improves maintainability and team collaboration
  • Without documentation:
    • Endpoint behavior must be inferred from source code
    • Increased onboarding time and integration errors

Swagger / OpenAPI

  • OpenAPI:
    • Specification for describing REST APIs
    • Defines endpoints, parameters, request bodies, and responses
    • Format: YAML or JSON
  • Swagger:
    • Tooling built around the OpenAPI specification
    • Provides an interactive documentation interface
  • Capabilities:
    • Visualizes the API structure
    • Allows requests to be executed directly from the documentation
    • Validates request and response formats
  • Acts as a contract between backend and clients

Example API documentation

  • Each documented endpoint specifies:

  • Path and method:

    • Example: GET /flights/:id
  • Parameters:

    • Path parameters and query parameters
    • Type and whether each is required
  • Request body:

    • Required fields and their types
  • Responses:

    • Structure of returned data
    • Possible status codes and their meanings

From theory to practice

  • Objective:
    • Apply the structured backend pattern to an existing API
  • Starting point:
    • Single-file Flights API with all logic inside app.js
  • Target:
    • Reorganize the project into routes, controllers, and models
    • Add new endpoints: POST /flights and DELETE /flights/:id
    • Document the API using Swagger

Practical: starting point

  • Initial Flights API characteristics:
    • All endpoints defined in app.js
    • In-memory array of flights
    • Two existing endpoints: GET /flights and GET /flights/:id
  • Limitations:
    • Routing, logic, and data access mixed in the same file
    • Adding new endpoints increases coupling and file size
    • No documentation available for clients

Refactoring plan

Before

flights-api/
  app.js
  package.json

After

flights-api/
  app.js
  package.json
  routes/
    flights.js
  controllers/
    flightsController.js
  models/
    flightsModel.js
  • Steps:
    1. Extract data access into a model module
    2. Move endpoint logic into a controller module
    3. Define endpoint paths in a router module
    4. Wire everything together in app.js

Model: data access

  • Holds the in-memory data and the operations that act on it
  • Exposes functions; does not interact with req or res
// models/flightsModel.js
let flights = [
  { id: 1, origin: "Seoul", destination: "Tokyo", date: "2026-06-01" },
  { id: 2, origin: "Seoul", destination: "Osaka", date: "2026-06-03" }
];

function findAll() { return flights; }
function findById(id) { return flights.find(f => f.id === id); }
function create(flight) { /* ... */ }
function remove(id) { /* ... */ }

module.exports = { findAll, findById, create, remove };

module.exports

  • Declares what this file makes available to others.
  • The object { findAll, findById, create, remove } exposes those four functions; any file that requires this module receives exactly that object.

Controller: request-handling logic

  • Receives the request, calls the model, builds the response
  • Contains validation and HTTP-level decisions (status codes)
// controllers/flightsController.js
const model = require("../models/flightsModel");

function getAll(req, res) {
  res.status(200).json(model.findAll());
}

function getById(req, res) {
  const id = Number(req.params.id);
  const flight = model.findById(id);
  if (!flight) return res.status(404).json({ error: "Flight not found" });
  res.status(200).json(flight);
}

module.exports = { getAll, getById };

require

  • Loads code from another file.
  • The path "../models/flightsModel" goes up one folder (from controllers/ to the project root), then enters models/ and loads flightsModel.js.
  • The result is stored in model and used as model.findAll(), model.findById(id), etc.

Routes: endpoint definitions

  • Maps HTTP methods and paths to controller functions
  • Uses Express Router to encapsulate route definitions
// routes/flights.js
const express = require("express");
const router = express.Router();
const controller = require("../controllers/flightsController");

router.get("/", controller.getAll);
router.get("/:id", controller.getById);

module.exports = router;

Wiring it together in app.js

  • app.js becomes a configuration entry point
  • Loads middleware, registers routers, starts the server
const express = require("express");
const flightsRouter = require("./routes/flights");

const app = express();
const PORT = 8080;

app.use(express.json());
app.use("/flights", flightsRouter);

app.listen(PORT, () => {
  console.log(`Flights API running on http://localhost:${PORT}`);
});

Adding POST /flights

  • Create a flight from JSON in the request body
  • Validate required fields and assign a new id
// controllers/flightsController.js
function create(req, res) {
  const { origin, destination, date } = req.body;
  if (!origin || !destination || !date) {
    return res.status(400).json({ error: "Missing required fields" });
  }
  const newFlight = model.create({ origin, destination, date });
  res.status(201).json(newFlight);
}

// routes/flights.js
router.post("/", controller.create);

Why 201 Created?

  • 201 indicates that the request was successful and a new resource was created as a result.
  • It is the standard response for a successful POST that creates a resource, and the response body typically contains the newly created resource (including its assigned id).

Adding DELETE /flights/:id

  • Remove a flight identified by its id
  • Return 404 if the flight does not exist
// controllers/flightsController.js
function remove(req, res) {
  const id = Number(req.params.id);
  const removed = model.remove(id);
  if (!removed) {
    return res.status(404).json({ error: "Flight not found" });
  }
  res.status(204).send();
}

// routes/flights.js
router.delete("/:id", controller.remove);

Why 204 No Content?

  • 204 indicates the request was processed successfully and there is no content to return in the response body.
  • It is the standard response for a successful DELETE, since the resource no longer exists and there is nothing meaningful to send back.

Adding Swagger to the project

  • Required packages:
    • swagger-ui-express — serves the interactive UI
    • yamljs — loads the OpenAPI specification from a YAML file
  • Setup in app.js:
const swaggerUi = require("swagger-ui-express");
const YAML = require("yamljs");
const swaggerDocument = YAML.load("./openapi.yaml");

app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument));
  • Documentation available at http://localhost:8080/api-docs

Documenting an endpoint

  • OpenAPI specification written in YAML
  • Each endpoint defines its method, parameters, and possible responses
openapi: 3.0.0
info:
  title: Flights API
  version: 1.0.0
paths:
  /flights/{id}:
    get:
      summary: Get a single flight by id
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: Flight found
        "404":
          description: Flight not found

Testing in Postman

  • Verify each endpoint after refactoring:
    • GET /flights — list of flights, status 200
    • GET /flights/:id — single flight or 404
    • POST /flights — create a flight, status 201
    • DELETE /flights/:id — remove a flight, status 204 or 404
  • Cross-check the same endpoints through the Swagger UI
  • Try adding flights, use the following JSON body as example with the POST method
   {
     "origin": "Seoul",
     "destination": "Fukuoka",
     "date": "2026-06-12"
   }
  • Try deleting a flight

Next week

  • Data persistence with SQLite
  • Replacing the in-memory model with a real database
  • Connecting with frontend

Acknowledgements


Back to title slide Back to lecture slides index