Let's take a look at the top Node.js frameworks. Each framework has its own strengths and features, but which would be best for creating a simple, real-time chat application? Nowadays, web applications need such features to keep users engaged, so building a real-time chat app is necessary.
Node.js is a great tool for building real-time apps because of its asynchronous and event-driven design. However, even within the Node.js ecosystem, there are many frameworks to choose from. How do you pick the right one for your specific project? It's important to choose the correct Node.js framework for your chat app. Your choice can affect your project's progress, performance, and scalability.
The list of frameworks to be analyzed was taken from the results of the stateofjs 2022 survey.
We have selected the top 5 Backend Frameworks listed as the most used:
Definition of the problem.
We list these requirements to make the problem more complex than a simple to-do. This helps us decide which Node.js frameworks are best for the solution. Then, we analyze each framework based on the information we gather.
Develop an application like a chat room that implements Real-Time Messaging. Users should be able to authenticate themselves. They should also create accounts, log in, and keep their identity during sessions. Messages should be delivered to the recipient in real-time without a manual refresh or page reload. The scope of these requirements does not include application deployment.
Express.js is a popular web application framework for Node.js. It's known for its simplicity and flexibility, making it a go-to choice for building web and mobile applications. Here's a brief overview:
- Middleware Support: Express uses middleware functions to handle requests and responses. This enables modular and customizable application design.
- Routing: It provides a robust routing system. Defining routes for different HTTP methods and URL patterns is easy.
- Template Engines: Express supports various template engines like Pug, EJS, and Handlebars. It's adaptable to other preferences.
- HTTP Utility Methods: It simplifies tasks with built-in methods for handling HTTP requests and responses.
- JSON Support: Express has built-in support for handling JSON data, making it easy to work with APIs.
- Extensibility: Developers can easily integrate third-party modules and packages to extend functionality.
- MVC Architecture: While not strictly enforcing MVC, Express supports the separation of concerns. Developers can organize their code in a way that follows the MVC pattern.
- Lightweight and Minimalistic: Express is designed to be unopinionated and minimal. Developers have the freedom to structure their applications as they see fit.
- Widespread Adoption: Being one of the most popular Node.js frameworks, it has a large and active community. There's extensive documentation, tutorials, and third-party modules.
- Flexibility: Express doesn't impose a rigid structure. Developers choose libraries and tools based on their project requirements.
- Performance: It's known for its fast performance. This capability makes it suitable for both small-scale projects and large-scale applications.
- Middleware: The middleware system enables developers to add functionality modularly. Code reusability is inherently promoted.
- Lack of Opinionation: While flexibility is an advantage, it can also be a drawback for developers who prefer a more opinionated framework with predefined conventions.
- Learning Curve: For beginners, the lack of strict guidelines might result in a steeper learning curve compared to more opinionated frameworks.
- Boilerplate Code: Since Express is minimalistic, developers might write more boilerplate code for specific features that other frameworks provide out of the box.
- Not a Full-Fledged Framework: Express focuses on the web server aspect, and developers often need to choose additional libraries for database interaction, authentication, and other standard features.
In this library, I chose the passport-local method. It's a Passport strategy for logging in and registering new users with a username and password. I picked it because I wanted the server to have a bigger role in the process. Other strategies in the library, like passport-http-bearer, passport-facebook, passport-google-oauth, passport-auth0, etc., mainly rely on a third party for this responsibility. I successfully established communication between the client and server using the mentioned libraries. I didn't encounter any significant problems, so I was able to implement the solution to the problem. There's still room for improvement in the application, the code contains what is necessary to fulfill our desired scope.
Nest.js is a progressive Node.js framework for building efficient, reliable, and scalable server-side applications. It makes developing robust, maintainable web applications and APIs more straightforward by incorporating the best practices from other frameworks and languages.
- Modularity: Nest.js encourages a modular structure. Developers can organize the code into reusable modules. This promotes a clean and maintainable codebase.
- Dependency Injection: Nest.js utilizes dependency injection, making it easy to manage and inject the dependencies of components. This enables modular and testable code.
- Express.js Compatibility: Nest.js is built on top of Express.js, a popular web application framework for Node.js. Nest.js applications can still leverage the extensive middleware ecosystem available for Express.
- Decorators: Nest.js uses decorators for defining and configuring modules, controllers, services, and more. This helps in writing concise and expressive code.
- WebSockets Support: Nest.js has built-in support for WebSockets, enabling real-time communication between the server and clients.
- CLI (Command Line Interface): Nest.js provides a powerful CLI for scaffolding and managing projects, making it easier to create components, modules, and more.
- Scalability: Nest.js is well-suited for small and large applications due to its scalable architecture.
- Code Reusability: The modular structure and dependency injection system enables code reusability. It's easier to maintain and extend applications.
- TypeScript Benefits: TypeScript brings static typing to Nest.js, improving code quality, providing better tooling, and catching potential errors during development.
- Express.js Compatibility: Since Nest.js is built on top of Express.js, it can leverage Express's existing middleware and ecosystem.
- Active Community: Nest.js has a growing and dynamic community. There is solid community support. Developers can find various resources and third-party packages easily.
- Learning Curve: For developers unfamiliar with concepts like decorators, dependency injection, or modular programming, there might be a learning curve when starting with Nest.js.
- Opinionated Structure: Nest.js enforces a particular structure and architecture, which might be seen as restrictive by some developers who prefer more flexibility.
- Size of the Community Ecosystem: While the community is active, it might not be as extensive as some other frameworks. The availability of specific plugins or extensions could be limited.
First, I started reading the documentation and running the suggested basic project. The documentation is very well organized and complete, especially concerning WebSockets, Mongoose Integration, and Authorization.
I had not used this framework before, and my first experiment was to program a server using Angular. At first, I thought that this framework had a stricter architecture than Express. I considered making a folder for the UI layer and another for the business logic and database interaction, which includes controllers and services. However, the documentation recommends organizing each module with their own DTOs and Schemas. So, I decided to follow that approach. This framework is designed to be developed using TypeScript, so I chose to use it despite the challenges. The framework ensures that the style errors be fixed. Although it offers the option to select between any of the three package managers, NPM, Yarn, and pNPM, I opted for the classic NPM.
I implemented the WebSockets logic in the same order as Express. The official documentation says there are two supported WS platforms: socket.io and ws. I looked at the Socket.IO implementation in the documentation because I wanted it to be compatible with the Express project. In that project, I had already used the Socket.IO library for real-time communication with the Angular client. I wanted to use the same client for all the Frameworks on the list. I used the @nestjs/jwt library. Initially, I had in mind to use Passport.js for the authentication logic. However, the documentation presented a more framework-friendly approach using the @nestjs/jwt library, which is described as "JWT utilities module for Nest based on the jsonwebtoken package. This library includes generating and verifying JWT tokens". I used Passport.js in Express mainly for its authentication strategies and the integration as a middleware. Mostly to extract the credentials and the JWT tokens from the client requests. These can be easily handled in Nest by using the JwtModule and passing the credentials to the services that are injected as dependencies. As in Express, use the bcrypt library to hash the password when storing it in the database.
I established the communication flow between the client and server without any problems. I say this because evaluating usability is essential. This particular framework seems to stand out for that.
Fastify is a web framework for building efficient and high-performance web applications in Node.js. It is designed to be lightweight, extensible, and easy to use.
- Performance-Oriented: Fastify is known for its exceptional performance. It achieves this by focusing on low overhead and fast execution. It's one of the fastest web frameworks for Node.js.
- Schema-Based Validation: Fastify uses JSON schema to validate request and response payloads. This helps ensure data consistency and provides a clear contract between the client and the server.
- Plugin System: The framework has a robust plugin system that allows developers to modularize their applications. This modularity is achieved by encapsulating functionality into plugins. Code reusability and maintainability are promoted.
- Asynchronous: Like many modern Node.js frameworks, Fastify is built to support asynchronous programming. It leverages features like async/await for handling asynchronous operations.
- Logging and Monitoring: Fastify includes built-in support for logging and monitoring. Tracking and debugging issues in your application is simple.
- HTTP2 and HTTP3 Support: Fastify supports the latest versions of the HTTP protocol, providing the benefits of features like multiplexing, header compression, and more.
- Performance: Fastify is renowned for its impressive performance, making it an excellent choice for high-throughput applications.
- Schema-Based Validation: Using JSON schema for validation enhances the robustness of applications by enforcing a clear structure for input and output data.
- Extensibility: The plugin system allows developers to extend and customize the functionality of their applications easily.
- Active Community: Fastify has a growing and vibrant community that contributes to plugins, documentation, and support. Developers can find resources and assistance easily.
- Asynchronous Support: Using Node.js's asynchronous nature, Fastify handles concurrent requests efficiently.
- Smaller Ecosystem: While the Fastify ecosystem is growing, it may not be as extensive as some other Node.js frameworks. This might limit the availability of specific plugins or integrations.
- Learning Curve: Developers familiar with other frameworks might face a learning curve when transitioning to Fastify due to its unique features and approach.
- Less Opinionated: While flexibility is an advantage, the less opinionated framework might require developers to make more project structure and organization decisions.
To begin with, I went through the general aspects of the documentation. I then initialized the project using the CLI and went through the structure of the resulting template. After that, within the documentation, I reviewed the three most important aspects of this project: WebSockets, MongoDB integration, and authentication.
Regarding WebSockets, Fastify offers two built-in options:
- @fastify/websocket: WebSocket support for Fastify. It is built upon wa.
- fastify-ws: WebSocket integration for Fastify — with support for WebSocket lifecycle hooks instead of a single handler function. It is built upon ws and uws.
The fastify-ws library is not being actively developed. Additionally, the @fastify/websocket library does not offer a built-in method to broadcast messages. It seems necessary to iterate over the WebSocket Server clients to send the message to all clients connected to the conversation. To keep the code as simple as possible and maintain uniformity with previous implementations, I opted to use the external libraries 'fastify-socket.io' and 'socket.io.' With this, the code used was practically reused from the Express project.
The integration with MongoDB, as in the previous frameworks, was through the Mongoose library, which is integrated as in Express.
The authentication methods use the libraries: jsonwebtoken to verify the token's validity concerning the JWT Secret, generate the Access Token and the Refresh Token, and bcrypt to hash the passwords before storing them in the database. Something that becomes evident while solving the first errors is that the number of results from Google searches are less numerous than in Express and Nest. That's a clear indication that this community is still growing. I followed the best practices recommended in the official documentation. I used schemas, decorators, and plugins for this. Schemas helped me validate request objects and responses. The documentation mentioned that using a schema can increase throughput by 10-20%. I registered the fastify-socket.io library as a plugin. I also used decorators to add extra functionality to the reply object. This allowed me to pass user data from the authorization service to the controllers. I also used decorators to define the 'authorize' function. On the other hand, in the authentication process, the services are executed in the preHandler. The controllers in the handler, using the preHandler, are suggested when we need to access the body content in the authentication process.
Strapi is an open-source headless content management system (CMS) that provides a flexible and customizable way to manage and deliver content through APIs. It allows developers to build robust, scalable web applications and digital experiences.
- Headless CMS: Strapi follows the headless CMS architecture, separating the backend (content management) from the frontend (presentation). This enables developers to use their preferred frontend technologies while benefiting from a robust content management system.
- API-First Approach: Strapi is designed with an API-first approach, allowing developers to create APIs easily and use them to deliver content to various channels, such as websites, mobile apps, and more.
- Customizable Content Types: Users can define content types and structures to suit specific project requirements. This flexibility makes it suitable for a wide range of applications and industries.
- User Authentication and Permissions: Strapi provides built-in user authentication and role-based access control (RBAC), making it easier to manage user roles and permissions for different application parts.
- Content Versioning: Strapi supports content versioning, allowing users to track changes made to content over time. This feature is helpful for content auditing and rollback purposes.
- Plugin System: The framework offers a plugin system that allows developers to extend functionality easily. A growing ecosystem of community-contributed plugins can be used to enhance Strapi's capabilities.
- Database Agnostic: Strapi is database-agnostic, which supports various databases such as MongoDB, PostgreSQL, MySQL, and SQLite. Developers can choose the database that best fits their project requirements.
- Open Source: Strapi is an open-source project that allows developers to use, modify, and distribute the software according to their needs.
- Flexibility: The customizable content types and API-first approach make Strapi suitable for various projects and use cases.
- Active Community: Strapi has a growing and dynamic community of developers, which means there is ample documentation, tutorials, and community support available.
- Easy to Use: Strapi offers an intuitive admin panel for content management, making it easy for non-technical users.
- Scalability: The headless architecture and support for various databases contribute to Strapi's scalability. It's suitable for both small projects and large enterprise applications.
- Learning Curve: While the admin panel is user-friendly, there might be a learning curve for developers new to Strapi or headless CMS concepts.
- Middleware Complexity: In some cases, managing complex middleware configurations may be challenging for developers.
- Community-Dependent: As an open-source project, the level of support and updates can depend on the community. However, the growing community is generally seen as a positive aspect.
Strapi is a tool for creating CMS and APIs quickly. At first, it didn't seem like a framework for making a Real-Time Chat application because its main focus is managing content and exposing it through APIs. But to evaluate it fairly, I looked at the documentation, especially the parts about WebSockets, MongoDB integration, and authentication.
The first thing I found was that, as stated in the documentation, "Starting from the release of Strapi v4, MongoDB is not supported natively anymore, and no connector is available". Because of compatibility issues between Strapi V4 and the socket.io library, I decided to downgrade to the latest release of V3, which is version "3.6.11". This version worked without any problems. I want to clarify that the reason I used different Node.js frameworks was to compare the client and database in a consistent manner. I also tried to use the socket.io library for WebSocket communication as much as possible.
The documentation is comprehensive, and I needed to consult doubts outside the official forums very few times. Unlike previous frameworks, Strapi offers an admin console in the browser for content management. Although the framework provides APIs to perform actions on users and collections, it is possible to create routes with custom logic to interact with plugins. I took advantage of this feature to define the Login and Signup routes to fit the format used in the Angular client. For authentication, I used the 'jsonwebtoken' library since I needed to customize the content of the tokens.
To connect to the MongoDB database, you just need to assign the right connector and other connection variables. Strapi has a query API to interact with the database. I used the 'socket.io' library to implement the WebSockets. The logic was the same as the previous frameworks. The only difference is how it's initialized. The logic for this library is placed in the bootstrap.js file. According to the documentation, the bootstrap.js file contains an asynchronous bootstrap function that runs before the application starts.
Finally, I could implement the proposed problem as in the previous frameworks. I believe that Strapi is not the appropriate framework for this type of application. That is to say that this type of application can be developed. But, all the functionalities the framework offers by default are being underused. The integrated logic could lead to unnecessary overhead when the application needs to be scaled.
- Middleware Flow: Koa uses a more modular and expressive approach to middleware. It relies heavily on async functions, making it easy to write and manage asynchronous code.
- Async/Await: Koa takes full advantage of ECMAScript 2017 (ES8) features, particularly async/await. Handling asynchronous operations without callbacks is simple.
- Lightweight Core: Koa has a small, focused core with many features moved to external middleware. Developers can include only the modules they need for their specific application, keeping the codebase clean and efficient.
- Context Object: Koa introduces a context object that encapsulates the request and response objects, providing a cleaner way to interact with the HTTP request and response.
- Clean and Readable Code: Koa encourages writing clean and readable code, especially when dealing with asynchronous operations. Using async/await simplifies handling asynchronous code compared to traditional callback-based approaches.
- Flexibility: Koa's modular architecture allows developers to choose and use only necessary components. This flexibility can lead to more lightweight and tailored applications.
- Learning Curve: Koa simplifies many aspects of web development. However, the heavy use of async/await and the different middleware approaches may create a learning curve for developers more accustomed to traditional callback-based frameworks.
- Ecosystem Size: While Koa has a decent ecosystem, it might not be as extensive as Express.js, which has been around for longer and is widely adopted. Finding specific middleware or community support might be a bit more challenging.
- Backward Compatibility: Koa 2 introduced some breaking changes compared to Koa 1, which might require modifications to existing codebases when upgrading.
As in previous frameworks, I started reading the most relevant aspects of the documentation. From this point, the minimalism appears obvious since the documentation is clear but not extensive. The three most relevant aspects are WebSockets, integration with the MongoDB database and Authorization. I implemented the external libraries Socket.io, Mongoose, and jsonwebtoken, respectively. At this point, it is to be expected that the application has been implemented without significant problems.
Koa seems to be the most similar to Express of all the frameworks discussed so far. However, Koa has a few notable differences. One difference is that it allows us to manage Middlewares with a flow downstream and upstream. This means we can perform actions after the subsequent middleware has finished executing. We can also do post-processing of the response or additional operations based on previous results. Koa is helpful for tasks like timing requests, applying response transformations conditionally, or cleaning resources.