Now that you have made a quality JD, it can still be tricky to evaluate the skills of your applicants when you hire ExpressJS developers. To help you with that, we have created a pool of questions that a good software developer should be comfortable with.
It is important to note that the ability to answer these questions doesn't imply that you have a top quality candidate. But it definitely is a big step in that direction.
To help you navigate through these questions, we’ve categorized the interview questions in 3 parts:
A. Basic concepts: Includes all basic concepts used across languages but we've focused on their significance in Node.js. This will give you an understanding of how strong their programming foundation is.
B. Advanced concepts: Includes all concepts that someone with higher expertise should know.
C. DS/Algorithm questions: To test the logical capability of the candidate.
A. Basic concepts
How is NodeJS asynchronous when javascript is single-threaded ?
We first need to understand that NodeJS is just a Javascript runtime built on Chrome’s V8 engine. The V8 engine was originally made by Google for Google. It was written in C++ to compile Javascript functions to machine code. It is highly efficient and optimised and is also used in Google Chrome for the same purpose.
Javascript is a single threaded language. This means that it has one call stack and one memory heap. Due to this it has to execute code sequentially. But as we know, the devils of synchronous programming can often decrease the efficiency of code. This is primarily due to blocking code which takes a long time to execute, such as I/O operations.
Request Handling in Browsers
To combat this problem, we have the different browser engines such as Chrome’s V8 engine, Firefox’s Spidermonkey, etc.. Tasks which take a long time are handled by Web API of these engines, which handle them in the background and allow synchronous execution of the rest of the code. When the background task in the Web API is done, a callback is pushed to the call stack.
Multi-threaded Request-Response Architecture
In other popular web application technologies such as JSP, Spring, ASP.NET, the language spawns a thread for each request and the thread executes the request and sends back a response. This is known as Multi-threaded Request-Response architecture.
Single-threaded Event Loop Architecture
NodeJS on the other hand follows a path similar to Javascript. NodeJS follows Single-threaded Event Loop architecture. It is built upon Javascript’s event based model with its callback mechanism.
The NodeJS web server internally maintains a thread pool to handle client requests, but these are not meant to be handled and used by the developer directly. The number of threads in the thread pool depends on the number of cores the CPU has. When the web server receives the requests, they are added to a queue known as ‘Event Queue’.
‘Event Loop’ is yet another internal component which is central to this architecture of NodeJS. Event Loop uses a single thread only. It runs indefinitely and checks the Event Queue for requests. If the request does not require any blocking I/O operation like handling file system, database, then the single thread directly executes them. But if there is any such operation then a thread is picked up from the thread pool, assigned to the request, and when it finishes puts a callback in the Event Loop.
Therefore, NodeJS runs on a single thread, similar to Javascript, but it maintains a thread pool from which it spawns threads to run asynchronous functions in the background.
B. Advanced concepts
What is a load balancer and how will you set it up?
First let us discuss why load balancers are required. As the traffic increases on a server, the server experiences a slow down. To prevent this slow down, the server needs to be scaled up. Scaling of server can be achieved in two ways:
- Vertical Scaling
- Horizontal 2Scaling
Vertical scaling refers to upgrading the resources of a particular server by increasing storage, increasing RAM, upgrading CPU. This gives the server more cores, faster execution speed, faster access time to storage and more storage. This was largely the traditional way of increasing performance of a server.
Horizontal scaling refers to using multiple servers to increase the performance of the back end infrastructure. So, instead of boosting resources of one server, multiple servers are used together. This has been found to be cheaper than vertical scaling and therefore practiced on a large scale. A load balancer is, thus, required in case of horizontal scaling.
In software development, load balancing refers to the process of distributing a set of tasks over a set of resources, with the aim of making their overall processing more efficient. Coming to backend development, load balancing refers to efficiently distributing incoming network traffic across a group of backend servers. Such a group of servers is called a server farm or server pool.
Along with distributing traffic load balancers also help in preventing a server from becoming a single point of failure. In a large scale architecture load balancers can be placed at 3 levels:
- between client and web server
- between web server and internal platform layer (like cache server or application server)
- between internal platform layer and database
But we are primarily going to focus on the first one.
Working
One server is taken primarily for load balancing. The requests first hit this server and then based on the load balancing algorithm the request is routed to one of the several back end servers, which carry out the request.
A load balancer considers two factors before forwarding a request:
- An algorithm that helps in choosing from the healthy servers
- If the server chosen is responding appropriately
The different algorithms used by a load balancer are:
- Least Connection Method: Direct traffic to the server with fewest active connections
- Least Response Time Method: Direct traffic to the server with fewest active connections and least response time
- Least Bandwidth Method: Direct traffic to the server serving the least amount of traffic (measured in Mbps)
- Round Robin Method: Directs requests to servers in a cyclic manner
- Weighted Round Robin Method: Different servers are weighed according to their resources, accordingly the one with more weights is given more priority.
- IP Hash: A hash is calculated of the IP address of the client. The server to forward the request to is chosen based on the hash value.
NGINX
A popular software used for load balancing is NGINX. It is an open source software for web serving, reverse proxying, caching, load balancing, media streaming, and more. It started out as a web server designed for maximum performance and stability. In addition to its HTTP server capabilities, NGINX can also function as a proxy server for email (IMAP, POP3, and SMTP) and a reverse proxy & load balancer for HTTP, TCP, and UDP servers.
It is quite simple to use NGINX as a load balancer. Install NGINX in the server chosen to be as the load balancer.
upstream app_servers {
server backend1.example.com;
server backend2.example.com;
server backend3.example.com;
}
server {
listen 80;
server_name localhost;
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host;
proxy_pass http://app_servers;
}
}
The upstream app_server block holds the servers to redirect requests to. Inside the location block, we are setting app_server as the target block. The server_name identifies which domain the load balancer will show.
Give a detailed overview of any one authentication strategy?
Web apps need to be authenticated to ensure that only the required people get access to the required data. Without strong authentication, you can suffer problems ranging from unauthorized login to complete leak of the database.
Authentication strategies are methods for user authentication and management. There are multiple ways to achieve this. With a constantly shifting landscape, it is difficult to zero in on one strategy as the best. Yet, it is a necessary element of almost all real world NodeJS projects.
The authentication strategies in NodeJS can broadly be classified into 3 types:
- Middleware Modules (like Passport.js)
- User Management as a Service
- Custom Database and Authentication
Middleware Modules
These are npm packages such as Passport.js, Everyauth and Permit. These are packages that work on top of NodeJS frameworks such as Express.js, Koa.js, Hapify.js. They utilise the ‘connect’ middleware conventions. This means that while using the aforementioned frameworks, you can just plug in the modules and use their functionality.
These mainly deal with session management, and also provide single sign on features. But their functionality is largely limited to this. They do not provide other features like account verification and password reset, which are left to the developer to develop.
Passport.js is by far the most popular amongst these packages. It is an authentication middleware designed primarily to authenticate requests and to safeguard endpoints/routes from unknown users. It can be implemented within an Express-based NodeJS web application.
Today, there are multiple ways of providing authentication: like a simple sign up/login or single-sign-on using an OAuth provider such as Facebook or Github, etc.. Passport offers a varied set of such mechanisms in the form of different packages, so that the developer doesn’t have to be burdened with extra dependencies.
app.post('/login', passport.authenticate('local', { successRedirect: '/',
failureRedirect: '/login' }));
As you can see in this route, passport.authenticate() is a middleware in the Express app route. In the passport authentication, ‘local’ option is opted which means using a custom username and password instead of OAuth through Facebook, Twitter or any other such provider. If the authentication is successful, then the user is redirected to the route ‘/’ and if it fails then the user is redirected to ‘/login’.
User Management as a Service
These are services such as Stormpath/Okta, Keycloak and Auth0. Over time, software has moved from on-premise to the cloud, to now a distributed API service. These technologies provide Identity Management APIs for software teams building web, mobile and API driven applications.
Identity management (ID management) is the organizational process for identifying, authenticating and authorizing individuals or groups of people to have access to applications, systems or networks by associating user rights and restrictions with established identities.
Such pre-built authentication and user management cut down cost & security risk of developing and maintaining identity in house. As a result, developers can focus on the core features instead. Compared to middleware modules, these provide more features than just securing endpoints. This includes email account verification, password reset workflows, role-based access, two-factor authentication and so on.
But introducing 3rd party services always has the requirement to be highly available, fast, portable, provide security during transport and most importantly provide flexibility based on the user model. These conditions vary vastly from service to service and an in-depth study needs to be done of all these before adopting a service.
Stormpath is one such service that offers identity management APIs. It is a cloud-based user data storage system with an option for private deployment. Features include user registration, authentication, authorization, user profiles, single-sign-on, multi-tenancy, token authentication, and API key management. It has an open source SDK present for most popular languages like NodeJS, Angular, Java, PHP, Python, Ruby, .NET, iOS, Android.
Stormpath also supports easy integration with Passport.js.
var spClient = new stormpath.Client({ apiKey: apiKey });
// Grab our app, then attempt to create this user's account.
spClient.getApplication(process.env['STORMPATH_APP_HREF'], function(err, app) {
if (err) throw err;
app.createAccount({
givenName: 'John',
surname: 'Smith',
username: username,
email: username,
password: password,
}, function (err, createdAccount) {
if (err) {
return res.render('register', { title: 'Register', error: err.userMessage });
}
passport.authenticate('stormpath')(req, res, function () {
return res.redirect('/dashboard');
});
});
});
});
This showcases a part of how working with Stormpath would be for a developer. Here the createAccount() feature of Stormpath is being showcased. As you can see that Passport.js is also being used and the ‘stormpath’ strategy is chosen.
Custom Database and Authentication
This is the more do-it-yourself approach, where the developer builds the whole authentication architecture from scratch. It first starts with choosing your tech stack, database, hashing algorithm, method of maintaining sessions and so on. After this, the next part would be to build the user management service.
It is a common task as it is intrinsic to almost any real world project. But it is prone to error - as anything built from scratch suffers from. Moreover, there is a hefty amount of code to be maintained too. But this strategy provides the most amount of flexibility and freedom to the developer.
A traditional stack for achieving this would be to use a database like MongoDB or PostgresQL. Pick the required ORM like sequelize or mongoose with them. Bcrypt is a popular hashing algorithm which also has npm packages for its easy implementation.
After this, routes need to be created for account creation, user verification, login and password reset. Thereon, an authentication logic is written to secure the different endpoints, and even the database needs to be secured from unauthorized access using secret keys.
C. Data Structure/ Algorithm
1. Given a string containing just the characters '(', ')', '{', '}', '[' and ']', determine if the input string is valid. An input string is valid if: Open brackets must be closed by the same type of brackets. Open brackets must be closed in the correct order. Note that an empty string is also considered valid.
class ValidParentheses{
func isValid(_ s: String) -> Bool {
var stc = [Character]()
for char in s {
if char == "(" || char == "[" || char == "{" {
stc.append(char)
} else if char == ")" {
guard stc.count != 0 && stc.removeLast() == "(" else {
return false
}
} else if char == "]" {
guard stc.count != 0 && stc.removeLast() == "[" else {
return false
}
} else if char == "}" {
guard stc.count != 0 && stc.removeLast() == "{" else {
return false
}
}
}
return stc.isEmpty
}
}
The above code will input 0(false).
2. What will the output of the following code be?
var a = 10;
var b = 5;
var c = 3;
if (a / b / c)
document.write("hi");
else
document.write("hello");
a) hi
b) hello
c) error
d) no output
The answer is A because the floating-point division returns a non zero value = 0.66 which evaluates
to true and outputs ‘hi’.