JavaScript for Server-Side ProgrammingIntroductionNode.js enables JavaScript to run outside the browser, making it suitable for web servers, APIs, command-line tools, background workers and real-time applications. However, server-side JavaScript requires more than familiarity with variables and functions. Backend programs must manage shared resources, asynchronous operations, failures, security boundaries and changing application state. This chapter develops the JavaScript concepts most important for maintainable Node.js and Express applications: scope, closures, objects, classes, functional programming patterns, error handling and modern syntax. The emphasis is not merely on language rules, but on how those rules influence backend architecture, testing, reliability and professional development practice. Learning ObjectivesBy the end of this chapter, you should be able to: - distinguish lexical, block, function and module scope;
- explain closures and apply them to configuration, factories and middleware;
- design objects and classes that represent backend responsibilities clearly;
- use destructuring, spread syntax, optional chaining, nullish coalescing and modules safely;
- apply functional patterns such as pure functions, higher-order functions and composition;
- handle synchronous and asynchronous errors in Node.js and Express;
- identify common maintainability, security and reliability problems in server-side JavaScript;
- evaluate a backend design using these concepts together.
1. JavaScript’s Server-Side Execution ModelA Node.js application is executed by the JavaScript engine inside a host environment that provides capabilities such as file access, networking, timers and process management. Node.js commonly uses an event loop: JavaScript executes on a main thread while many input/output operations are delegated to the operating system or internal worker facilities. When an operation completes, its callback or continuation is placed into a queue for later execution. This model is efficient for I/O-bound applications because the program does not need to block while waiting for a database or network response. It does not mean that all work is automatically non-blocking. CPU-intensive loops, synchronous file operations and large in-memory transformations can still delay every request being served by the process. A useful distinction is: | Operation | Typical effect | Backend implication |
|---|
| Reading a file asynchronously | Returns control while I/O proceeds | Usually suitable for request handling | | Reading a file synchronously | Blocks JavaScript execution | Avoid in request paths | | Awaiting a database query | Suspends the current async function | Other event-loop work may continue | | Large CPU calculation | Occupies the JavaScript thread | Consider optimization, workers or a separate service |
The language features in this chapter help developers control state and asynchronous flow. They do not remove the need to understand the runtime model. 2. Scope, Bindings and LifetimeScope determines where a binding can be accessed. A binding is a name associated with a value, such as const port = 3000. JavaScript uses lexical scope: accessibility is determined by where code is written, rather than by where a function is called. 2.1 let, const and varlet and const are block-scoped. A block is commonly delimited by braces, such as those belonging to an if statement, loop or function. const prevents reassignment of the binding, although an object or array referenced by that binding may still be mutable.
const config = { port: 3000 };
config.port = 4000; // permitted: object mutation
// config = {}; // TypeError: binding reassignment
let requestCount = 0;
requestCount += 1; // permittedvar is function-scoped and may be redeclared. Its historical behaviour, including hoisting and interaction with loops, makes it more error-prone in modern applications. New backend code should normally use const by default and let only when reassignment is required.
A declaration is hoisted in the sense that the binding is created before execution reaches the declaration. let and const remain inaccessible in the temporal dead zone until execution reaches the declaration. Accessing them earlier produces a ReferenceError. This is safer than silently receiving undefined, a common source of bugs with var. 2.2 Shadowing and global stateA nested scope may define a binding with the same name as an outer binding. This is called shadowing. const environment = 'production';
function report() {
const environment = 'test';
return environment;
}Although legal, excessive shadowing reduces readability. Accidental global variables are more dangerous. In Node.js modules, top-level variables are local to that module rather than automatically global, but explicitly using global state can still make tests order-dependent and create race conditions between requests. Prefer passing dependencies as arguments or constructing them in a controlled composition root. For example, a service that receives a repository is easier to test than one that secretly imports and modifies a global repository. 2.3 Module scopeEach CommonJS module has its own scope. CommonJS commonly uses: const express = require('express');
module.exports = createServer;ECMAScript modules use import and export: import express from 'express';
export function createServer() {
return express();
}The project configuration determines which module system is active. A team should choose deliberately and apply one consistent convention. Module scope is valuable because it prevents implementation details from becoming globally visible. 3. Closures and Controlled StateA closure is a function together with access to variables from its surrounding lexical environment, even after the surrounding function has returned. Closures are fundamental to middleware factories, configuration, counters, caching and dependency injection. function createLimiter(maximum) {
let used = 0;
return function allow() {
if (used >= maximum) return false;
used += 1;
return true;
};
}
const allowThree = createLimiter(3);The returned function closes over used and maximum. These values are not directly accessible to other code, which provides a basic form of encapsulation. Closures are not automatically thread-safe or request-safe. In a single Node.js process, asynchronous operations can interleave, and shared mutable state may still produce logical errors. A module-level counter may also be incorrect when an application runs in multiple processes or containers. For durable counts, use a suitable external store or database. 3.1 Closures in Express middlewareMiddleware factories use closures to capture configuration: function requireRole(role) {
return function roleMiddleware(req, res, next) {
if (!req.user || req.user.role !== role) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
}
app.get('/reports', requireRole('manager'), reportController);The factory receives configuration once; the middleware receives request-specific data later. This separation improves reuse and keeps policy visible at the route definition. 3.2 Closures in loopsA frequent historical problem occurred when var was used in asynchronous callbacks inside loops because all callbacks shared one function-scoped binding. let creates a new binding for each iteration in a loop, avoiding that particular problem. Nevertheless, modern code should prefer clear iteration methods and async/await where appropriate. 4. Objects and Data ModellingObjects group related data and behaviour. In backend systems, objects commonly represent request data, configuration, domain entities, service dependencies and response structures. const user = {
id: 42,
email: '[email protected]',
isActive: true,
deactivate() {
this.isActive = false;
}
};The this value in a method call such as user.deactivate() refers to user. Arrow functions do not create their own this; they capture it from the surrounding scope. Therefore, arrow functions are useful for callbacks but should not be substituted automatically for object methods that depend on dynamic this. 4.1 Object compositionBackend code often benefits from composition: assembling small objects with focused responsibilities rather than building a deep inheritance tree. function createUserService({ userRepository, passwordHasher }) {
return {
async register(input) {
const hash = await passwordHasher.hash(input.password);
return userRepository.insert({ email: input.email, passwordHash: hash });
}
};
}The service receives dependencies explicitly. A test can provide fake implementations without changing production code. 4.2 Property shorthand, computed properties and immutabilityModern syntax reduces repetition: const id = 7;
const status = 'active';
const record = { id, status };
const field = 'email';
const projection = { [field]: '[email protected]' };Spread syntax creates a shallow copy or combines properties: const updatedUser = { ...user, isActive: false };This does not deep-copy nested objects. If user.preferences is an object, both versions may refer to the same nested object. Developers should understand this distinction before relying on spread syntax for isolation. Object freezing can prevent some accidental mutation: const settings = Object.freeze({ timeout: 5000 });Freezing is shallow and is not a complete security mechanism. It also does not replace validation of input received from clients. 5. Classes and EncapsulationA class defines a pattern for constructing objects with shared methods and state. class UserAccount {
#email;
constructor(email) {
this.#email = email.toLowerCase();
}
get email() {
return this.#email;
}
changeEmail(newEmail) {
if (!newEmail.includes('@')) {
throw new Error('Invalid email');
}
this.#email = newEmail.toLowerCase();
}
}The #email field is private to the class. Private fields provide language-level access control, unlike a convention such as _email, which remains publicly accessible. Classes can be useful for domain objects, database clients and components with lifecycle state. They are not inherently more professional than functions. A class may be inappropriate when a plain object and a few functions communicate the design more clearly. Backend teams should choose based on responsibility, state and testability rather than style preference. Inheritance can express an “is-a” relationship, but deep inheritance creates coupling. Composition is often preferable. If two services share a small operation, extracting a utility or delegating to a collaborator is frequently clearer than creating several subclasses. 6. Modern JavaScript Syntax for Backend CodeDestructuringDestructuring extracts values from objects and arrays: const { host = 'localhost', port = 3000 } = config;
const [first, second] = values;Defaults apply when the value is undefined, not when it is null. This distinction matters when processing database results or HTTP payloads. Optional chaining and nullish coalescingOptional chaining safely stops a property access when an intermediate value is null or undefined: const city = user.profile?.address?.city; Nullish coalescing supplies a fallback only for null or undefined: const pageSize = request.query.limit ?? 20; This differs from ||, which also replaces valid falsy values such as 0, false and the empty string. For example, a limit of 0 may have a specific meaning and should not automatically become 20. Rest parameters and spreadRest parameters collect remaining arguments: function audit(event, ...metadata) {
return { event, metadata };
}Spread expands an iterable or object. It is useful for constructing values, but it does not validate or sanitize data. Never assume that spreading client input makes it safe for a database query or response. Promises and async/awaitA Promise represents the eventual result of an asynchronous operation. An async function always returns a Promise, and await unwraps a fulfilled Promise or throws its rejection reason. async function loadUser(id, repository) {
const user = await repository.findById(id);
if (!user) return null;
return user;
}Independent operations can be run concurrently with Promise.all: const [user, preferences] = await Promise.all([
userRepository.findById(id),
preferenceRepository.findByUserId(id)
]); Use this only when the operations are independent and the combined concurrency is acceptable. Promise.all rejects when any input Promise rejects. If every result must be observed regardless of failure, Promise.allSettled may be more appropriate. 7. Functional Patterns for MaintainabilityFunctional programming does not require eliminating all mutation. It encourages explicit data flow, predictable functions and limited side effects. A pure function returns the same output for the same input and does not modify external state or perform observable side effects. function calculateTotal(items) {
return items.reduce((total, item) => total + item.price * item.quantity, 0);
}The function assumes each item has numeric price and quantity; production code should validate these assumptions before calculation. Higher-order functions accept functions, return functions, or both. Common examples include map, filter and reduce: const activeEmails = users
.filter(user => user.isActive)
.map(user => user.email); These methods improve clarity when each transformation is simple. A long chain containing hidden side effects can be harder to debug than an explicit loop. Composition and middlewareFunction composition combines small operations. Express middleware is a practical example: each function receives a request, response and continuation function, then either completes the response or passes control onward. Good middleware should have one clear purpose, avoid unexpected mutation and document values it adds to the request. A useful design distinction is between pure domain logic and effectful adapters. Validation rules, price calculations and permission decisions can often be pure. Database calls, logging, network requests and file access are effects that should be isolated at system boundaries. 8. Error Handling and ReliabilityErrors are expected events in backend systems. They may result from invalid input, missing resources, authentication failure, database unavailability, programming defects or operational limits. 8.1 Synchronous errorsUse try...catch when a synchronous operation may throw or when an error must be translated: try {
const data = JSON.parse(rawText);
return data;
} catch (error) {
throw new Error('Request body is not valid JSON', { cause: error });
}Do not catch an error merely to ignore it. Either recover, add useful context and rethrow, or allow a central handler to process it. 8.2 Asynchronous errorsawait allows Promise rejections to be handled using try...catch:
async function controller(req, res, next) {
try {
const result = await service.find(req.params.id);
if (!result) return res.status(404).json({ error: 'Not found' });
res.json(result);
} catch (error) {
next(error);
}
}Express applications should have a centralized error-handling middleware. It should log appropriate diagnostic information, avoid exposing stack traces in production responses and return consistent error formats. Operational errors may be translated into a safe client message; programming errors should be recorded for correction. Never expose passwords, tokens, database credentials or internal stack traces to clients. Logging must also consider privacy regulations and organizational policy. 8.3 Error classesCustom error classes communicate categories: class NotFoundError extends Error {
constructor(resource) {
super(`${resource} was not found`);
this.name = 'NotFoundError';
this.statusCode = 404;
}
}The HTTP status code should be selected by the application boundary, not blindly copied from arbitrary client input. Validation errors commonly produce a 4xx response; unexpected failures commonly produce a 5xx response. 8.4 Error-handling principles- Validate input at trust boundaries.
- Preserve the original cause when adding context.
- Avoid broad catches that conceal defects.
- Do not continue after an operation has left state uncertain.
- Use timeouts and cancellation for external calls where supported.
- Log correlation identifiers rather than sensitive payloads.
- Test both expected failures and unexpected dependency failures.
9. Illustrative Case: A Reliable User Registration EndpointBackgroundConsider a fictional online learning platform. Its registration endpoint receives an email and password, checks whether the email is already registered, hashes the password and stores a new account. ProblemAn initial implementation performs all work in one route handler. It uses a module-level database connection, returns raw database errors, and sometimes creates duplicate accounts when two requests arrive close together. AnalysisThe route has too many responsibilities: parsing, validation, business rules, persistence, error translation and response formatting. The duplicate problem cannot be solved reliably by an application-level check alone because concurrent requests may both observe that an email is absent. A database uniqueness constraint is required as the final authority. ApproachSeparate the layers: function createRegistrationService({ users, hasher }) {
return {
async register({ email, password }) {
if (typeof email !== 'string' || typeof password !== 'string') {
throw new ValidationError('Email and password are required');
}
const normalizedEmail = email.trim().toLowerCase();
if (!normalizedEmail.includes('@') || password.length < 12) {
throw new ValidationError('Registration data is invalid');
}
const passwordHash = await hasher.hash(password);
try {
return await users.insert({
email: normalizedEmail,
passwordHash
});
} catch (error) {
if (error.code === 'UNIQUE_EMAIL') {
throw new ConflictError('Email is already registered', { cause: error });
}
throw error;
}
}
};
}The controller can now call the service and pass errors to centralized middleware. The repository owns database details, while the service owns registration policy. A unique database constraint protects against concurrent inserts. Tests can inject fake users and hasher dependencies. OutcomeThe design improves maintainability because each component has a narrower responsibility. It improves security by storing a password hash rather than the password, although the selected hashing algorithm and cost must be appropriate for the deployment environment. It improves reliability through database enforcement and consistent error translation. Learning points- Scope and dependency injection reduce hidden coupling.
- Pure validation logic can be tested independently.
- A closure can configure a service without exposing internal dependencies.
- Application checks and database constraints serve different purposes.
- Error messages returned to clients should not reveal unnecessary internal details.
10. Limitations, Security and Professional PracticeJavaScript’s flexibility can become a liability when types, mutation and asynchronous control flow are poorly managed. Use linters, formatters and automated tests to enforce team conventions. TypeScript may add compile-time checking for larger projects, but it does not replace runtime validation because HTTP clients and databases remain external sources of data. Prototype pollution is a security concern when untrusted keys are merged into objects or used to modify prototypes. Use well-maintained validation libraries, allow-list expected fields and avoid unsafe deep merges. Do not construct database queries by concatenating user input. Dependency management is also part of backend professionalism. Pin or control dependency versions, review advisories, remove unused packages and apply least privilege to process credentials. Environment variables are useful for configuration, but secrets should be managed through an appropriate secret-management system rather than committed to source control. Modern Node.js development increasingly includes structured logging, observability, request tracing, graceful shutdown, cancellation and worker-based processing for CPU-heavy tasks. These practices extend the language concepts in this chapter into production reliability engineering. Chapter SummaryServer-side JavaScript depends on disciplined management of scope, state and asynchronous work. const and let provide block-scoped bindings, while module scope limits visibility and reduces global coupling. Closures preserve access to lexical state and support middleware factories, configuration and dependency injection, but shared mutable state must be designed carefully. Objects and classes model data and behaviour; composition is often more flexible than deep inheritance. Destructuring, spread syntax, optional chaining, nullish coalescing, Promises and async/await make modern backend code expressive, provided their boundary conditions are understood. Functional patterns encourage pure logic, explicit transformations and isolation of side effects. Reliable Express applications validate input, distinguish expected from unexpected failures, use centralized error handling and avoid exposing sensitive details. In professional systems, language features work together with database constraints, testing, observability, security controls and clear architectural boundaries. Key TermsScope: The region of code in which a binding can be accessed. Lexical scope: Scope determined by where code is written. Block scope: Scope created by a block and used by let and const. Module scope: Visibility limited to a module unless explicitly exported. Closure: A function together with access to its surrounding lexical environment. Binding: A named association between an identifier and a value. Hoisting: Creation of declarations before normal execution reaches them. Shadowing: Defining an inner binding with the same name as an outer binding. Object: A collection of properties and, potentially, methods. Class: A construct for defining object instances and shared behaviour. Composition: Building a component from smaller collaborating components. Encapsulation: Restricting direct access to internal state or implementation. Pure function: A function without observable side effects that gives consistent results for the same inputs. Higher-order function: A function that accepts or returns another function. Promise: An object representing the eventual result of an asynchronous operation. Middleware: A function that participates in processing a request-response pipeline. Exception: An abnormal condition represented by a thrown value. Dependency injection: Supplying a component’s collaborators from outside the component. Idempotence: A property in which repeating an operation has the same intended effect as performing it once. Trust boundary: A point where data enters from a less-trusted source and must be validated. Review QuestionsShort Answer Questions- What is lexical scope, and how does it differ from global state?
- Why is
const not equivalent to deep immutability? - Define a closure and give one backend use case.
- What is the difference between optional chaining and nullish coalescing?
- Why should Express applications use centralized error-handling middleware?
Long Answer Questions- Explain how scope, closures and dependency injection can be combined to create testable Express services.
- Compare object composition and class inheritance in a Node.js backend. Include advantages and limitations of each.
- Explain how
async/await, Promise.all and error propagation influence concurrent backend operations. - Discuss the role of pure functions and side-effect isolation in maintainable server-side code.
- Describe a complete error-handling strategy for an API, from input validation to production logging.
Analytical / Application Questions- A route handler validates input, queries a database, hashes a password and formats an HTTP response in one 80-line function. Analyse the design and propose a more maintainable decomposition.
- Two registration requests for the same email arrive simultaneously. Explain why an application-level existence check may fail and design controls that prevent duplicate records.
- An API uses
request.query.limit || 20 to choose a page size. Analyse the behaviour for 0, an empty string, null and undefined, then recommend an appropriate implementation.
Multiple Choice Questions with AnswersQuestion 1Which declaration is block-scoped and cannot be reassigned? A. var B. let C. const D. function Correct Answer: C. const Question 2What does a closure preserve? A. Only global variables B. Access to its lexical environment C. The database connection automatically D. A copy of every object in the program Correct Answer: B. Access to its lexical environment Question 3What is the main purpose of dependency injection? A. To hide all errors B. To make collaborators explicit and replaceable C. To make every function asynchronous D. To eliminate database constraints Correct Answer: B. To make collaborators explicit and replaceable Question 4What does optional chaining help prevent? A. All validation errors B. Access errors when an intermediate value is nullish C. All Promise rejections D. Prototype pollution in every situation Correct Answer: B. Access errors when an intermediate value is nullish Question 5What does value ?? fallback do when value is 0? A. Returns fallback B. Throws an exception C. Returns 0 D. Converts 0 to null Correct Answer: C. Returns 0 Question 6What happens when one Promise in Promise.all rejects? A. The combined Promise rejects B. All Promises are retried automatically C. The rejection is ignored D. The combined Promise always fulfils Correct Answer: A. The combined Promise rejects Question 7Which is a characteristic of a pure function? A. It modifies a module-level counter B. It depends on the current time C. It produces consistent output for the same inputs D. It must use a class Correct Answer: C. It produces consistent output for the same inputs Question 8Where should unexpected Express errors normally be processed? A. In client-side browser code B. By centralized error-handling middleware C. By ignoring the rejected Promise D. In a CSS file Correct Answer: B. By centralized error-handling middleware Question 9Why is a database uniqueness constraint important for user registration? A. It formats JSON responses B. It prevents all invalid passwords C. It enforces uniqueness during concurrent writes D. It replaces password hashing Correct Answer: C. It enforces uniqueness during concurrent writes Question 10What does object spread generally create? A. A deep copy of every nested object B. A shallow copy with copied enumerable properties C. An immutable database record D. A private class field Correct Answer: B. A shallow copy with copied enumerable properties
|