Mastering The Basics of Express Js

Mastering The Basics of Express Js

Taking a step forward in the world of backend development

Introduction

Express is a minimal, fast, and flexible web application framework for Node.js. It simplifies backend development by handling routing, middleware, and HTTP tasks, and writing server-side logic much easier making it ideal for building APIs and web apps efficiently. I highly recommend reading this blog on Node js before proceeding.

Creating an express app

const express = require('express')
const app = express() //Creates an express app

//Responds to requests coming from / ie home page
app.get('/', (req, res) => { 
  res.send('<p>Home Page</p>')
})

//sending a file as response
app.get('/about', (req, res) => { 
    res.sendFile('./views/about.html', { root: __dirname }); 
};

//Redirect
app.get('/about-me', (req, res) => {
    res.redirect('/about');
});

//404 page
app.use((req, res) => {
    res.statusCode = 404;
    res.sendFile('./views/404.html', { root: __dirname });
    //res.statusCode(404).sendFile('./views/404.html', { root: __dirname });
    //This is same as above two statements because, the res.statusCode(404) 
    //returns a res object
});

app.listen(3000) //Listens for requests on port 3000
  • app.get('route', callback): The app.get method in Express.js is used to define a route that listens for incoming HTTP GET requests to a specific URL path. When a matching request is received, the associated callback function is executed, allowing you to define how the server responds to that particular request.

  • app.use(callback): The app.use() method in Express.js is used to mount middleware functions on the application's request-response cycle ( We will learn about middleware in the upcoming sections). The use function fires for every single request that is coming in, but only if the request reaches the method. By that, we mean that the associated callback is executed solely when no response has been sent before reaching this code line.

This is just so simple when compared to the method of setting up a server, request handling with listeners, and navigating requests using switch statements in Node.js.

View engine

Until now, everything we did was service static html pages. By static, we mean that all of the content of the html page is predefined and doesn't change. Sometimes we want to inject some dynamic data into the web pages something like the user data on the home page on login. How do we do that? To do that, we use something called the view engine.

In Express.js, a view engine is a template rendering mechanism that enables dynamic content generation on the server side. It allows you to create dynamic web pages by combining static content with variables and logic within your templates. Express.js supports various view engines like EJS, Pug (formerly known as Jade), Handlebars, and more. These engines provide a way to structure and generate HTML, making it easier to display dynamic data from your server to the client.

We will be using EJS going forward in this blog.

const express = require('express')
const app = express() 
app.set('view engine', 'ejs'); //Setting ejs as view engine

app.get('/', (req, res) => { 
   res.render('index');
})

app.get('/about', (req, res) => { 
    res.render('about');
};

app.get('/blogs/create', (req, res) => { 
    res.render('create');
};

app.use((req, res) => {
    res.statusCode(404).render('404');
});

app.listen(3000);
  • The app.set() method in Express.js is used to configure application settings. It allows you to define various properties that affect how your application behaves, such as setting the view engine, specifying the views directory, enabling trust proxy, and more. These settings influence Express's behavior and help customize your application's functionality.

  • By default, express looks for the view folder for different views. We can change the default view to some other folder by using app.set('views', 'directory-name').

  • The view folder has different views. It has an extension .ejs. ejs stands for embeded javascript. In an .ejs file, you can embed JavaScript code within HTML using special tags. These tags help you insert dynamic values, loop through data, conditionally render content, and more.

  • We use res.render('file') to render the html onto the DOM.

Passing data into views

  • We can write JS inside the <% %> tags. This js is executed on the server side and not on the client side.

  • To access a single value of any variable, we use <%= variable %> tag.

  • We can pass data to a view from the express app using the render method. The render method expects the data object as the second argument.

      res.render('index', { title: 'Express JS'});
    

    We can use the value of the title inside the view like this

      <p> Blog Page | <%= title %> </p>
    
  • Now let us look at how to handle comples data inside views

      const blogs = [
          {title: 'Yoshi finds eggs', snippet: 'Lorem ipsum dolor sit amet consectetur'},
          {title: 'Mario finds stars', snippet: 'Lorem ipsum dolor sit amet consectetur'},
          {title: 'How to defeat bowser', snippet: 'Lorem ipsum dolor sit amet consectetur'}
      ];
      res.render('index', { title: 'Express JS', blogs});
    
      <% if (blogs.length > 0) { %>
          <% blogs.forEach(blog => { %>
              <h3 class="title"><%= blog.title %></h3>
              <p class="snippet"><%= blog.snippet %></p>
          <% }) %>
      <% } %>
      <% else { %>
          <p>No blogs to display.... </p>
      <% } %>
    

How does EJS work?

The EJS templates we have written will be passed to the EJS view engine to further process the view. The view engine looks for any kind of dynamic content, variable, loops, or conditionals and when it finds them it does conditional rendering and at the end, it splits out a valid html page. The html page is returned to the browser. This whole process is called server-side-rendering.

Partials

Partials in Express refer to reusable components or templates that are designed to be included within other templates. They allow you to break down your web page into smaller, manageable pieces, making your code more modular and maintainable.

In the context of view engines like EJS or other templating systems used with Express, partials are used to create common sections of a web page, such as headers, footers, navigation menus, or sidebars, which are then included in multiple pages.

Here's how partials work in Express:

  1. Creating Partials: You create separate .ejs files for each component you want to reuse. These files contain the HTML and embedded JavaScript code necessary for the component.

  2. Including Partials: In your main .ejs template files, you use special syntax provided by the view engine to include the partials. This syntax depends on the view engine you're using (for example, <%- include('path to partial.ejs') %>).

  3. Dynamic Insertion: When rendering the main template, the view engine replaces the include syntax with the content of the corresponding partial, effectively inserting the partial's HTML code into the main template.

Middlewares

Middleware is basically a name for any code that runs on the server between getting a request and sending a response.

Middleware runs from top to bottom sequentially until we exit the process or explicitly send a response to the browser. For example, we have a request to the '/' route.

We run the code (middleware) from top to bottom checking if we have a response for the '/' route. If we encounter such a block of code, we send a response back to the browser. The code after that is never executed. Hence the order of middleware is very important.

Middleware examples/use cases:

  • Logger middleware to log details of every request

  • Authentication check middleware for protected routes

  • Middleware to parse JSON data from requests

  • Return 404 pages, etc

So the 404 page we created earlier is an example of a custom middleware. The reason why we placed it at the end of the page is because

  1. We wanted it to execute only if other middleware wasn't executed

  2. And we didn't have anything to do after it.

When we place a middleware app.use() at the beginning, it is executed for every request and the execution stops there, It does not do what to do next i.e. it neither exits nor goes to the next statement, the control just sits there. We have to explicitly tell express what to do next using a method called next().

We get next method as the third argument of the callback function. And we just invoke at the end of middleware. It passes the control to the next middleware function in the chain.

app.use((req, res, next) => {
    console.log('New request made:');
    console.log('host: ', req.hostname) ;
    console.log('path: ', req.path);
    console. log('method: ', req.method);
    next();
});

Third-party middlewares

Third-party middleware in Express refers to pre-built middleware functions that are not included with Express itself but are developed and maintained by the community or external sources. These middleware extend Express's functionality, enabling tasks like authentication, request parsing, logging, and more. By integrating third-party middleware, developers can enhance their applications with additional features without having to build these functionalities from scratch.

For example, Morgan is a popular third-party middleware for Express that provides logging capabilities for incoming HTTP requests. It captures details about each request, such as the HTTP method, URL, response status, response time, and more, and outputs this information to the console or a specified log file.

const morgan = require('morgan');
app.use(morgan('dev');

output: :method :url :status :response-time ms - :res[content-length]

You can read more about the morgan middleware here.

Static Files

Static files such as images, external stylesheets, javascript files etc cannot be sent to the browser just like we send html files. This is because the server protects all of the files automatically from users, which is why we can't access any of the files if we wish to.

To allow the browser to access something, we have to specify which files are allowed to be accessed by the browser. This can be done using the static middleware that comes along with express.

const express = require('express');
const app = express();

// Serve static files from the "public" directory
app.use(express.static('public'));

What this means is that all files inside the public folder is public and can be used by the browser.

Interacting with a Database

A database is a structured collection of data that is organized, stored, and managed in a way that enables efficient retrieval, manipulation, and storage of information. Databases are designed to facilitate the storage of large amounts of data, provide mechanisms for querying and analyzing data, and ensure data integrity and security.

We can either use a SQL or a No SQL database.

  • SQL database: An SQL database is a type of database that uses the Structured Query Language (SQL) for managing and manipulating data. Here data is stored in the form of tables.

  • No SQL database: A NoSQL database stores data in the form of collections and documents. A collection is similar to a table and a document is similar to columns of a table in an SQL database.

We wil be using a No SQL database. MongoDB is a popular open-source NoSQL database management system that is designed to handle unstructured or semi-structured data in a flexible and scalable way. It stores data in JSON-like documents with dynamic schemas.

Mongo DB setup with Atlas

Setting up MongoDB involves several options depending on your development and deployment requirements.

  • We can install MongoDB locally on our computer, develop and deploy that, OR

  • We can use a cloud database that is already deployed and it is much more easier for us to manage. One such cloud service is Mongo DB Atlas.

You can set up Mongo DB Atlas here.

  1. Login/Signup into Mongo DB

  2. Build a new cluster (Stick to default settings).

  3. Create a database under the collections tab. Create desired collections in the database.

  4. Go to the database access tab and create a new database user. You will be accessing the database from your express app using this username and password.

  5. Go back to the clusters tab and get your connection string. We will use this in our express app.

const express = require('express');
const app = express();
const dbURI = 'connecion string'; //Use your connection string here

Mongoose

Mongoose isn't just a database connector; it provides advanced features, making developers' lives easier. One of its standout features is the ability to create what it calls "object modelling capabilities”.

Firstly, let's understand MongoDB basics.

Data in MongoDB is organized into collections, which, in turn, consist of documents. Collections store multiple documents, and each document holds data in a JSON-like format, referred to as BSON. Now remember that MongoDB is a schema-less database.

Here's where Mongoose steps in. It offers schema objects that are tied to MongoDB collections. Schemas allow us to define a structure for our documents, specifying the types of data each ocument should contain.

For example, we can say a document's flight number must be a number, and the mission name should be a string. We can even enforce that certain fields are required.

Models in Mongoose: To leverage these schemas, we create models. A model applies a schema to a specific collection in MongoDB. It essentially enforces the structure defined in the schema. With our models in place, we can seamlessly query our MongoDB collections using Node.js. Mongoose provides a way to perform all CRUD (Create, Read, Update, Delete) operations on our data.

Connecting to Mongo DB

We can connect to the database using the regular Moongo DB API package and use it to write queries to the database too, but that is a lot of job and can get harder. So we use something called Mongoose which is an ODM (Object-Document Mapping**)** library. ODM libraries allow you to work with MongoDB using JavaScript objects, abstracting away some of the database complexities. Mongoose wraps the standard Mongo DB API and it provides us with a much easier way to connect to and query the database.

We work with mongoose in the following way

  1. Creating Schema:

    The schema defines the structure of the data/collection of the database. It contains properties and property types of each collection.

  2. Creating a model based on the schema:

    Model allows a user to communicate with the database collection. The model contains static and dynamic methods that allow us to communicate with the database.

     const mongoose = require('mongoose');
     const Schema = mongoose.Schema;
     //Creating schema
     const blogSchema = new Schema({
         title: {
             type: String,
             required: true
         },
         snippet: {
             type: String,
             required: true
         },
         body: {
             type: String,
             required: true
         }
     }, { timestamps: true });
    
     // Creating Blog model using the schema
     const Blog = mongoose.model('Blog', blogSchema);
     module.exports = Blog;
    
    • Schema(): This method is used to create a schema and it takes in two arguments. The first is an object defining its properties and the second is the options objects. Here in the option object, we are setting timeStamps: true. This adds two additional fields, createdAt and updatedAt, to automatically track the creation and modification times of each document.

    • model(): This method is used to create a model using a schema. It takes in two arguments: the first is the name of the model and the second is the schema.

  3. Connecting to the database using Mongoose:

     const express = require('express');
     const mongoose = require('mongoose');
    
     const app = express();
     const dbURI = 'connecion string';
    
     mongoose.connect(dbURI,{ 
         useNewUrlParser = true,    //Used to parse the connection string
         useFindAndModify = false,  
         useCreateIndex = true,     
         useUnifiedTopology = true  
         })
         .then(result => app.listen(3000))
         .catch(err => console.log(err));
    
    • The second argument of the mongoose.connect() is the options object. Without that, if we run the server, we will get DepricationWarning.

    • mongoose.connect() is an asynchronous operation and returns a promise.

    • We are placing the app.listen() inside the callback function because we want to listen to the requests only after the application connects to the database.

  4. Querying the database:

     const Blog = require('./models/blog');
     //Creating a new blog and saving it to DB
     app.get('/add-blog', (req, res) => {
         const blog = new Blog({
             title: 'Mastering express js',
             snippet: 'My new blog',
             body: 'lorem ipsum .....'
         });
         blog.save() //Saves blog to the Blog collection
             .then(result => { res.send(result)) })
             .catch(err => { console.log(err) });
     });
    
     //Fetching all documents of a collection
     app.get('/all-blogs', (req, res) => {
         Blog.find().sort({ createdAt: -1 })//Sorts in ascending order(Optional)
             .then(result => { 
                 res.render('index' { title: 'All blogs', blogs: result })) 
             })
             .catch(err => { console.log(err) });
     });
    
     //Fetching a single document of a collection
     app.get('/all-blogs/fsdhsdjfnjksjf4', (req, res) => {
         Blog.findById('fsdhsdjfnjksjf4')
             .then(result => { res.send(result)) })
             .catch(err => { console.log(err) });
     });
    

    The code is fairly simple and self-explanatory.

Request Types

Here are some common types of requests you might encounter when working with a database:

  1. GET: Request to get a resource.

  2. POST: Request to create a new data.

  3. DELETE: Request to delete data.

  4. PUT: Request to update data.

We are unknowingly familiar with GET requests. For example, when we search for localhost:3000/blogs, this is nothing but a GET request to fetch html pages belonging to that route. Let us look into other types of requests.

  1. POST Request:

    The POST request is an HTTP method used to send data to a server for the purpose of creating a resource. It's commonly used to submit form data, JSON payloads, or other types of data to a web server.

    When we submit forms, we can use the action and the method attribute for post request. The action attribute in HTML is used to specify the URL or endpoint to which the form data should be sent when the user submits the form.

     <form action="/blogs" method="POST">
         .
         .
         <button>Submit</button>
     </form>
    
     app.use(express.urlencoded()); 
     app.post('/blogs', (req, res) => {
         const blog = new Blog(req.body);
         blog.save()
             .then(result => res.redirect('/blogs')
             .catch(err => console.log(err))
     });
    

    express.urlencoded() is a middleware that takes all the URL-encoded data coming from a POST request and parses that into an object that can be used in the request object. All the values of data are attached to req.body.

    Before learning about DELETE and PUT requests, let us first look into something called route parameters.

  2. Route Parameters:

    Route parameters are flexible components within a route that can vary in value. For instance, in the URL "localhost:3000/blogs/:id," the term "id" functions as a route parameter, marked by the use of ":" in its prefix. The value of "id" can differ, adapting to different blog entries, such as "localhost:3000/blogs/1," "localhost:3000/blogs/2," and so on. This mechanism enables dynamic handling of distinct resources under a unified route structure.

     app.get('/blogs/:id', (req, res) => {
         const id = req.params.id; //Getting route parameter (id)
         Blog.findById(id)
             .then(result => {
                 //Details is a view of a specific blog
                 res.render('details', { title: 'Blog Details', blog: result});
             })
             .catch(err => console.log(err));
     });
    
  3. DELETE Request:

    A DELETE request is a method used to request the removal of a resource identified by a specific URL or ID.

    For example, deleting a blog by clicking the delete button.

     <a class="delete" data-doc="<%= blog._id %>">delete</a>
    
     <script>
         const trashcan = document.querySelector('a.delete');
         trashcan.addEventListener('click', (e) => {
             const endpoint = `/blogs/${trashcan.dataset.doc}`;
             fetch(endpoint,  { method: 'DELETE'})
                 .then(response => response.json())    
                 .then(data => window.location.href = data.redirect)
                 .catch(err => console.log(err));
         });
     })
     </script>
    
    • We can add a delete button like this in the details view. We have added a custom attribute data-doc which will be used in our app to send a delete request.

    • Inside the script tag, we are writing code to send a delete request to the server when we click the delete button. fetch in JavaScript returns a promise. We will look at what is happening in .then() block after the server code.

    app.delete('/blogs/:id', (req, res) => {
        const id = req.params.id; 
        Blog.findByIdAndDelete(id) //Deletes document having 'id'
            .then(result => {
                res.json({ redirect: '/blogs' });
            })
            .catch(err => console.log(err));
    });
  • Here we are listening to delete requests and deleting a document. After deleting the id, we are not directly redirecting to the 'blogs' page because when we send an asynchronous request (fetch in this case), we cannot use a redirect method in node.js, we must return a JSON object to the browser. So we are returning the path to which we want to redirect to the browser.

  • In details view, the response object in .then refers to the JSON object. response.json() parses the JSON data into a JavaScript object and returns a promise. We then append another then method that receives the parsed js object. Using window.location.href, we redirect to the 'blogs' page.

Express Router

Express Router is a routing system that's built into the Express.js web framework. It provides a way to modularize and organize your route-handling code in a more structured manner. Instead of defining all your routes in a single file, Express Router allows you to separate your routes into different files and manage them in small groups of routes that belong together.

For example, We have handled a lot of routes which has /blogs in common. So we can group all of them and handle it in a single file. Let the 'router' folder contain all the route handlers and let us group all the files that serve the /blogs.... routes in a file called blogRoutes.js.

So we can just cut and paste all the route handlers with /blog.. route from app.js into blogRoutes.js file. But this isn't gonna work because all of them contain app.get() or app.post() etc., which reference the app object from the main application file. To address this, we use the Router() method provided by Express.js.

const express = require('express');
const router = express.Router();

app.get('/blogs/...', callback)  -> router.get('/blogs/...', callback)
app.post('/blogs/...', callback) -> router.post('/blogs/...', callback)
app.use('/blogs/...', callback)  -> router.use('/blogs/...', callback)

module.exposts = router;

router is like a mini-app, it behaves like the app, but stands alone and does nothing. We have to use this router inside an app. So in app.js, we import this router instance and use it with app.

const blogRoute = require('./routes/blogRoutes.js'); //Importing route 
app.use(blogRoute);

The line app.use(blogRoute) mounts this router instance onto your main application. This means that any routes defined within blogRoutes.js using methods like router.get() or router.post() will be accessible under the "/blogs" path when you navigate to them in your browser or make HTTP requests.

We can go one step further and refine how we use the blogRoute in your Express.js application. We can scope the blogRoute to a specific URL.

const blogRoute = require('./routes/blogRoutes.js'); //Importing route 
app.use('/blogs', blogRoute);

By writing app.use('/blogs', blogRoute), you are binding the blogRoute to a specific URL path, namely "/blogs." This means that the route handlers within blogRoute.js will be triggered only when you visit routes starting with "/blogs."

With this approach, you no longer need to include "/blogs" in each route handler definition within blogRoutes.js. The scoping at the application level already ensures that these routes are associated with the "/blogs" URL segment.

router.get('/blogs/...', callback)   -> router.get('/...', callback)
router.post('/blogs/...', callback)  -> router.post('/...', callback)
router.use('/blogs/...', callback)   -> router.use('/...', callback)

This way, your code remains concise, and the router instances are well-structured and aligned with their respective URL paths.

MVC Basics

MVC stands for Model, View and Controller. It is a way of structuring our code and files. It keeps the code modular, reusable and easier to read as the application size increases.

We already know what model and view are. Controllers are things that form the link between the model and the view. They act as a middleman that uses the model to get data and pass that data into the view. We have already done this directly in route handlers without the use of controllers, but the idea of using controllers is that we just extract those handler functions into a separate controller file that can be referenced by the route functions later. The use of controllers is not mandatory and completely optional, the idea behind using controllers is to make the code reusable and easy to understand.

NOTE: By route handlers here is nothing but the callback function in app.get() or app.use().

So now, the route file matches the route for incoming requests and passes those requests to the appropriate controller function. The controller communicates with the appropriate model and gets the required data and passes it into the view. The view renders its template and sends it to the browser.

The above code snippet is an example of how to use MVC. We can do the same thing to all the routes.

const express = require('express');
const blogController = require('../controllers/blogController');

const router = express.Router();

router.get('/', blogController.blog_index);
router.post('/', blogController.blog_create_post);
router.get('/create', blogController.blog_create_get);
router.get('/:id', blogController.blog_details);
router.delete('/:id', blogController.blog_delete);

module.exports = router;

Now the express router will look something like this. So clean and easy to read right?

What are REST API's?

REST stands for REpresentational State Transfer. It is a software architecture that imposes conditions on how APIs should work. When we follow REST for building APIs, we call it RESTful APIs. The REpresentation and the State talk about how the server makes data available and the Transfer talk about how the data is being transferred.

RESTful APIs have the following features

  • Using existing standards(HTTP, JSON, URL)

  • Use GET, PUT, POST and DELETE requests to communicate the action we are performing on the database.

  • Client and server architecture.

  • Requests are stateless and cachable.

Conclusion

In this blog, we've covered essential aspects of Express, from creating an app to handling databases and MVC fundamentals. With these insights, you're ready to build powerful web applications. Happy coding!