Intro & Slides
00:00:00This updated crash course on Express.js builds upon a previous version from five years ago, offering an improved learning experience. It is recommended for beginners to first understand Node.js through the linked crash course before diving into this one. The video covers foundational concepts of Express, including its purpose and usage in web development, followed by practical demonstrations such as creating routes and handling requests.
What is Express?
00:00:53Express is a minimal, flexible web framework for Node.js that simplifies building server-side applications, APIs, and microservices. It’s widely used in backend development within stacks like MERN (MongoDB, Express, React, Node) or MEAN/MEVN by swapping front-end technologies such as Angular or Vue. Known as the go-to framework for its ease of handling HTTP requests compared to low-level modules like HTTP in pure Node.js setups; it provides robust tools making API creation straightforward. Trusted by major companies including IBM and Uber while also being popular among startups.
Opinionated vs Unopinionated
00:02:28Web frameworks can be categorized as opinionated or unopinionated. Opinionated frameworks, like Ruby on Rails and Laravel, provide structured rules and conventions with built-in features such as authentication and database integration. They guide developers in a specific direction but limit flexibility. In contrast, unopinionated frameworks like Express.js offer minimal structure, allowing developers to choose their own tools and application architecture while requiring additional effort for added functionality.
Prerequisites
00:04:10To effectively work with Express, a solid understanding of JavaScript fundamentals and ES6 features like arrow functions, destructuring, promises, and async/await is crucial. Basic knowledge of Node.js suffices—watching a concise crash course can be enough. Familiarity with npm for installing packages and handling JSON data or using template engines such as EJS or Pug is also important. Whether building server-side applications or APIs in the M Stack (MongoDB-Express-Angular/React), mastering JavaScript should always come first.
What we'll cover
00:05:18The focus is on setting up an Express server, exploring routing, and understanding the request-response cycle. Key topics include handling headers and response bodies, creating custom middleware for tasks like logging, implementing CRUD operations to build a full API, and using EJS template engines for rendering HTML pages directly from the server. Additional aspects cover error handling with custom middleware solutions, organizing code through controllers to streamline route files, fetching data via frontend JavaScript interacting with the backend API through static file servers. Environment variables are also introduced alongside other essential tools.
Express Setup
00:06:45Express.js is introduced as a framework with comprehensive documentation and guides on routing, middleware, and more. To begin using it, install Node.js from nodejs.org if not already installed. Create an empty project folder in your text editor (e.g., VS Code), initialize the project with `npm init -y` to generate a default package.json file, then install Express via `npm install express`. Set up essential files like .gitignore to exclude sensitive or unnecessary data such as node_modules or environment variables when pushing code to repositories.
Basic Server
00:09:00Setting Up a Basic Express Server To create an entry point for a server, name it appropriately based on its purpose (e.g., "server.js" for APIs or JSON data). Use CommonJS syntax to require the Express module and initialize it into an app variable. The app object is used to define routes, middleware, and start the server using `app.listen()`. For example, listening on port 8000 with a console log confirms that the server is running.
Defining Routes in Express Define simple routes using methods like `app.get()` which handle specific HTTP requests such as GET. A route function takes request and response objects; use these to access headers or send responses via methods like `.send()`. Responses can include plain text, HTML tags parsed automatically by browsers without specifying content type manually. Additionally, you can return JSON directly from JavaScript objects without manual stringification.
watch Flag & NPM Scripts
00:13:10Restarting the server after every change can be tedious, but recent versions of Node.js offer a solution using watch flags. By creating custom npm scripts in package.json, developers can automate this process without relying on third-party tools like nodemon. A 'start' script runs the application normally while a 'dev' script uses "node --watch" to enable automatic restarts upon file changes. This feature simplifies development by eliminating manual server restarts and allows for seamless updates during coding.
res.sendFile() Method
00:15:25The method res.sendFile() is used to serve specific HTML files in response to HTTP requests. By creating a 'public' folder containing the desired HTML files, such as index.html and about.html, you can define routes that send these pages using absolute paths. The path module simplifies this process by constructing file paths dynamically through methods like __dirname and path.join(). This approach demonstrates how individual routes can be set up for each page but highlights that static servers are generally more efficient.
Static Web Server
00:18:00Using the express static middleware simplifies serving HTML files by designating a folder as 'static.' This eliminates the need to create individual routes for each file. By setting up a public directory and using app.use(express.static), any file placed in this folder can be accessed directly via its URL without additional routing setup. For example, adding an "about.html" or "contact.html" into this designated folder makes them immediately accessible through their respective URLs.
Working with JSON
00:19:48To work with JSON data using Node.js, start by setting up an endpoint prefixed with 'API' for convention. Define the route (e.g., '/api/posts') and use hardcoded JavaScript objects to simulate database entries. Create an array of posts containing IDs and titles, then serve this data through the endpoint using `res.json()`. This method ensures that your response is properly formatted as JSON. The resulting API can be accessed via browsers or integrated into applications like React or mobile apps.
Postman Utility
00:22:35Testing APIs effectively requires tools that support various HTTP methods like POST, PUT, and DELETE. Postman is a preferred choice due to its versatility; it offers both a desktop version and an extension for Visual Studio Code (VS Code). After installing the free extension from the marketplace and creating an account, users can initiate new HTTP requests directly within VS Code. For example, sending GET requests to endpoints such as localhost:8000/api/posts retrieves JSON data with headers detailing content type and other metadata.
Environment Variables (.env)
00:23:45Environment variables are system-level variables that can be defined in a file named .env for secure storage, such as API keys or port numbers. Previously requiring third-party packages like dotenv, modern versions of Node.js now support using .env files natively. To implement this, create a .env file with your desired variable (e.g., PORT=8080), and ensure it is excluded from version control by adding it to the gitignore file. In your server code, access these values via process.env.VARIABLE_NAME while providing fallback defaults if needed (e.g., const port = process.env.PORT || 8000). Finally, explicitly specify the use of the .env file within package.json scripts to activate its functionality.
Request Params (req.params)
00:26:30To retrieve a single post dynamically, create a route with an ID parameter prefixed by a colon (e.g., '/:id'). Use the request object's 'params' property to access this dynamic value. Convert the string ID into an integer using `parseInt` and filter posts based on whether their IDs match the provided one. If no matching post is found, return an empty array or ideally respond with a 404 error message.
Query Strings (req.query)
00:29:35Extracting and Validating Query Strings in Backend Development Query strings, such as 'limit=2', can be accessed using the request.query object. These query parameters are parsed into an object where each key-value pair represents a parameter from the URL. To ensure data integrity, developers must validate user inputs to prevent issues like SQL injection or unexpected behavior. For instance, when expecting a positive number for limiting posts returned by an API endpoint, it's essential to parse it correctly (e.g., using parseInt) and verify conditions like positivity and numeric validity with functions such as isNaN.
Implementing Conditional Responses Based on Query Parameters By validating input values from query strings (like ensuring 'limit' is both numeric and positive), APIs can dynamically adjust their responses based on these parameters. Using methods like slice(), developers can limit arrays of data according to specified constraints while providing fallback behaviors if no valid limits are provided—returning all items by default otherwise.
Setting Status Codes
00:33:19HTTP status codes are essential for communicating the outcome of a request. The 200 range indicates success, 300 signifies redirects, 400 represents client errors, and 500 denotes server errors. While res.json defaults to sending a status code of 200, explicitly specifying it improves readability. To handle cases where resources like posts do not exist (e.g., ID mismatch), use conditional checks with appropriate responses such as a "404 Not Found" along with an explanatory message.
Multiple responses
00:36:40To streamline code, avoid using unnecessary 'else' statements after a return within an if block. By removing the else and directly returning in the first condition, you maintain functionality while improving readability. This approach ensures clarity without altering how conditions are evaluated or executed.
Route Files
00:37:35To manage a growing number of routes efficiently, it's essential to move them into separate files. A 'routes' folder is created at the root level, and individual route files like 'posts.js' are added. Express Router is used by importing it from Express and replacing app references with router within these new route files. The router object must be exported using module.exports for compatibility with CommonJS modules. In the main server file (server.js), these modularized routes are imported using require() and integrated as middleware via app.use(). This setup allows defining specific endpoints without redundancy while keeping the codebase clean.
Using ES Modules
00:41:40To transition from CommonJS to ES modules, set the "type" field in package.json to "module." Replace require statements with import syntax, such as importing Express and path using 'import' instead of 'require.' Ensure file extensions like .js are included when referencing files. Update exports by replacing module.exports with export default for compatibility. After making these changes across relevant files (e.g., server.js and route files), test functionality to confirm everything works seamlessly.
Request Body Data
00:43:47Handling Request Body Data in Express To handle request body data for a POST request, middleware is added to parse JSON and URL-encoded form data. Using `express.json()` enables raw JSON parsing, while `express.urlencoded({ extended: false })` allows handling of URL-encoded forms. A new route is created using the router's `.post` method at the endpoint `/api/posts`, where incoming body data can be accessed via `req.body`. This simplifies earlier methods that required manual buffering and string conversion.
Creating Posts with Middleware Integration A POST route processes requests by logging received title values from the client-side form submission through Postman or similar tools. The response sends back status 201 (created) along with an acknowledgment object containing submitted post details like 'title'. While no database integration exists yet, posts are temporarily stored in memory arrays for demonstration purposes.
POST Request
00:47:53In an unopinionated framework like Express, creating a new post involves defining an object with properties such as ID and title. The ID can be generated by adding one to the current length of posts, though databases typically handle this automatically. Validation ensures that if no title is provided in the request body, it returns a 400 status code with an error message prompting for inclusion of a title. If valid data exists, the new post is added to an array but lacks persistence since it's not stored in a database.
PUT Request
00:50:23To implement a PUT request for updating posts in a CRUD API, the endpoint is defined as '/api/:id', where ':id' represents the post ID to be updated. The server retrieves this ID using 'req.params.id', parses it into an integer, and locates the corresponding post from an array with '.find()'. If no matching post exists, it returns a 404 status with an error message. Upon finding the correct post, its title is updated using data from 'req.body.title', followed by sending back a response containing the modified object.
DELETE Request
00:53:23To implement a delete request, the ID of the post is parsed as an integer. The array of posts is filtered to exclude any post matching that ID, effectively removing it from the list. This updated array without the deleted item is then sent back as a response. In real-world applications, such operations are typically performed using databases like MongoDB with Object-Document Mappers (ODM) such as Mongoose or SQL databases for more efficient and less verbose code.
Middleware
00:55:44Understanding Middleware Functions in Node.js Middleware functions are versatile tools in Node.js, granting access to request and response objects. They serve various purposes like logging, authentication, or modifying the request object (e.g., adding a 'req.user' for authenticated users). These functions take three parameters: request, response, and next—a function that triggers the subsequent middleware in the stack.
Implementing Logger Middleware at Route and App Levels A logger middleware can log details such as HTTP method and URL of incoming requests. It can be applied either at a specific route level by passing it as an argument or globally across all routes using app.use(). For better organization, it's common practice to store middlewares separately (e.g., 'logger.js') before importing them into server files for application-wide usage.
Custom Error Handler
01:00:24Creating a Custom Error Handler in Express Express's default error handler returns an HTML page for errors, which is unsuitable for APIs. To address this, you can create a custom middleware-based error handler that provides JSON responses with appropriate status codes. By defining the `errorHandler` function and integrating it into your application using `app.use`, specific messages and statuses are returned based on the context of each route.
Implementing Dynamic Status Codes and Messages The custom error handler dynamically sets response statuses by checking if an explicit status code exists; otherwise, defaults to 500 (server error). Routes pass errors via the next() function along with relevant details like message or status code. This approach centralizes handling logic across routes while ensuring accurate feedback such as '400 Bad Request' when required fields are missing.
Catch All Error Middleware
01:08:30To handle non-existent endpoints, a catch-all error middleware is implemented. By adding an app.use function after defining all routes, any undefined endpoint triggers this middleware. It creates an error object with the message 'Not Found' and status 404 before passing it to the next handler for processing. To streamline code organization, this functionality can be moved into a separate file (e.g., notFound.js) and imported where needed.
Colors Package
01:10:47To make console logs more visually distinct, the 'colors' library is introduced. After installing it via npm and adding it as a dependency in package.json, colors are applied to log messages based on HTTP methods. A mapping object assigns specific colors: green for GET, blue for POST, yellow for PUT, and red for DELETE requests. This approach avoids repetitive if-else statements by dynamically selecting the color using request.method within an array structure.
Using Controllers
01:14:17Organizing Logic with Controllers To streamline the request-response cycle, functionality is moved from route files to dedicated controller methods. A new folder named 'controllers' houses a file called postController.js where functions like getPosts, createPost, updatePost, and deletePost are defined using arrow functions. Each function includes descriptive comments detailing its purpose and associated routes for clarity. This approach keeps route files clean by simply pointing endpoints to corresponding controller methods.
Implementing Clean Code Practices in Routes The newly created controller methods are exported individually or collectively at the bottom of their respective files for modularity. These controllers are then imported into the routes file to replace inline logic with references to these cleaner method names (e.g., getPosts). Testing confirms that all functionalities remain intact while improving code readability and maintainability through this structured separation of concerns.
__dirname Workaround
01:20:45When using ES modules, the '__dirname' variable is not defined as it is in CommonJS. To address this issue, a workaround involves importing 'fileURLToPath' from Node.js's URL module and creating your own '__filename'. This can be achieved by passing 'import.meta.url' to 'fileURLToPath', which converts the file URL into a usable path string. The directory name ('__dirname') can then be derived using Node.js's Path module with its dirname method applied to the generated filename. Once implemented correctly, static files like HTML pages are served without errors.
Making Requests From Frontend
01:24:29Fetching Data with Vanilla JavaScript The process of fetching data from a backend API using vanilla JavaScript involves creating an HTML structure and linking it to a frontend script. A button is added for triggering the fetch operation, while an output element displays the fetched posts. The main.js file contains functions that use async/await syntax to make HTTP requests via Fetch API, handle errors gracefully with try-catch blocks, and dynamically update the DOM by appending post elements.
Dynamic Post Display on Button Click When users click the 'Get Post' button in this setup, an event listener triggers a function that retrieves posts from localhost:8000's endpoint. Posts are processed into JSON format and displayed as individual div elements within the designated output area on-screen. This approach ensures seamless integration between user interaction (button clicks) and dynamic content rendering without relying on external frameworks like React or Axios.
Submit Form to API
01:30:03Creating a Form for Adding Posts A simple form is added to the HTML with an ID of 'add post form', containing input fields and a submit button. This allows users to add new posts by interacting with the backend API from the frontend.
Implementing Post Submission Logic in JavaScript An asynchronous function, 'addPost,' handles submitting data via POST requests using Fetch API. It prevents default behavior, gathers input values as JSON-formatted data, sends it to an endpoint ('http://localhost:8000/api/posts'), and appends newly created posts dynamically on successful submission.
EJS Template Engine Setup
01:36:00To integrate the EJS template engine into an Express application, start by creating a new project directory and initializing it. Install both 'express' and 'ejs' as dependencies using npm. Configure the server to use EJS by setting its view engine property to "ejs" and specifying a views folder for templates. Create basic routes in your app.js file that render specific templates from this folder, such as an index.ejs page displaying HTML content like "Welcome." Run the server locally on port 8000 to see rendered output.
Pass Data to Views
01:41:15EJS template engines allow rendering HTML pages with dynamic data. By passing an object as a second argument, variables like 'title' and 'message' can be defined and used in the view. The syntax involves embedding these variables within angle brackets, percent signs, and equals symbols (e.g., <%= variable %>). This enables displaying dynamic content such as titles or messages directly on rendered web pages.
Pass and Loop Over Arrays
01:42:50EJS allows dynamic data rendering by passing variables like arrays into templates. For example, an array of names such as John, Jane, and Jack can be passed to create a list dynamically in HTML using the 'forEach' loop syntax within EJS tags. This approach is particularly useful for projects requiring simple UI updates without needing highly interactive or complex interfaces.
Template Partials
01:44:20EJS allows for the creation of reusable components, such as headers, across multiple pages using partials. By organizing these shared elements in a 'partials' folder within the views directory and including them via syntax like '- include', developers can avoid duplicating code. This approach simplifies maintenance and ensures consistency throughout an application.