Transcripts
1. Class Introduction: Welcome back to Module ten, logging and handling errors of the Express JS course series. My name is Shawn Ragawnhi, You guide to
mastering Express Js. Over the years, I have built scalable and robust
applications for various industries,
and in this class, I will help you tackle one of the most critical aspects
of backend development, logging and handling errors. One of my proudest moments
was when I designed a logging system that reduced debugging time for
a client by 60%. Today, I will show you how
to achieve similar results. In this module, we will focus on identifying and handling errors
efficiently in Express s, simplifying error handling with tools like Express async errors, logging errors to
multiple destinations like files and MongoDB
for better tracking. Next, handling uncaught
exceptions and unhandled rejections to
ensure application stability. And finally, we will extract and organize key parts
of your app that is routes logging and configuration for a clean
and maintainable code base. This class is for developers
who want to build robust and maintainable
Express Jas applications. If you have completed
the earlier modules, you are ready to dive in. Basic knowledge of JavaScript no Jas and express is all
you need to get started. By mastering error
handling and logging, you will significantly enhance the reliability of
your applications. You will also save
countless hours debugging, making you a more efficient
and valuable developer. Finally, for the project, you will build a custom lover to handle logs across
multiple transports. That is Console
Files and Manga DB. And then you will refactor the Fair Wheels
app by extracting joy validation and server
logic into separate modules. These hands on
tasks will solidify your understanding of error
handling and modularization. This module is a game changer for your backend
development skills. Let's dive in and make your applications more
robust than ever. See you in the first
lecture, let's get started.
2. Introduction: Logging and Handling Errors: In our current implementation
of Fairwee's app, we have assumed an ideal world where everything
works successfully. However, in the real world, there are always
unexpected errors. For example, it is possible
that our connection to Mongo Deb drops out
for whatever reason. So as a best practice, you should count for these
unexpected situations and handle them properly, which means you should send a proper error message to the client and log the
exception on the server. So later, you can look at
the log and see what are some issues that are happening frequently and how
you can improve them. So let me demonstrate a real world scenario where
our Mongo EV server dies. So here in the terminal, you can see I'm running the
application with Nord mod. And here's my other
terminal window where I'm running Mongo Damon. It is a background
service that is listening on port 27017. And here in Postman, I have a tab open to send
a get request api CR. So when we send this request, we get a 200 response. Beautiful. Now, back
in Mongo DB terminal, I'm going to stop this process. So let me open a new
Power Show window and type top services name MongoDB. And that's it. Our Mongo DV server
is now shut down. Let's see what happens when you send this request
one more time. So this is hanging in there. After a few seconds,
we are going to see an error message in the terminal where we are
running the application. Okay, so here's
the error message. Mongo server selection error, SRO selection timed out after, then we have a dynamic
value server selection, timeout S. And here's the actual error
interrupted at shutdown. So by default, when you
connect to Mongo DB, if the connection
cannot be established, MongoDB driver will attempt to reconnect three times
with 1 second interval. If you scroll down to
the bottom of the page, C, our app crashed. That means we exited from
the process with code one. Now in this particular
demonstration, I shut down the MongaiVserver, so it wouldn't really matter if this process is live or not. But let's imagine in a
real world scenario, our Manga Divi server is
going down for, let's say, 1 minute, and then it's going to come back after 1 minute. With the current implementation, our node process will
terminate and will not be able to serve any other clients
even after Mongo Divi resta. So this is a problem
and a very big one. So we need to properly
handle these scenarios, and that's what we are going
to learn in this section.
3. Handling Server Selection Error: Now let's take the first step to handle this error properly. So whenever you see
server selection error, that means Manga B
client is unable to connect to any server in
the Mangaib deployment. This happens for various reasons like incorrect connection rings, network issues, server
misconfiguration or version incompatibility. Note that this error occurred when we tried to fetch
data from the car's API. So let's go to our cars module. This is the handler for
getting the list of cars. So here we have a promise
that is returned here. We are awaiting that, but
nowhere in this code, we have a try cache block to
handle rejected promises. This implementation is
same as getting a promise, calling then, but not calling
cash to handle the errors. So if you are using
the promised syntax, we should always call cache
to handle exceptions. If you are using a
sync and Aviate, you should always
have r cache blocks. So here, we need to put this
code in a tri block like this and then add the cache
block where we get the error. Now here we need to send a
proper response to the client. So response that status. We use the error code 500, which means internal
server error. So something failed on the
server, we don't know what. And then send a message
like something failed. Now, technically, here we
should also log the exception, but we are going to
look at that later. So let's improve this
application step by step. Now, let's see how this
new implementation works. So back in the terminal, I'm going to start Mangaibi first because if we don't start it and
run the application, look, we cannot
connect to Mangaib. So initially, I want
to be connected to Mangaib and then I want to drop that connection
somewhere in the middle. So let's stop this application. Now back to the terminal window. Let's run start
service name Mangaib. Now let's run our fair
wheels application. Okay, connected to Manga DV. I'm going to start this process. So again, top service
name Manga DV. Then here in Post MD, I'm going to send a request
for the cars endpoint. This is going to
take 30 seconds, so I'm going to pause the
recording and come back. Alright. This is what we get. 500 internal server error. And this is our message. Now, if you look at the terminal window for our application, you no longer see that
server selection error, which resulted in the
termination of this process.
4. Error Middleware in Express: So in the last lecture, we took the first step to
handle errors properly. But there is a problem in
the current implementation. Let's say tomorrow
we decide to change the message that we
send to the client. With the current implementation, we have to go through every
route handler where we use this Trcche block
and modify that message. So in a real world situation, here we are going
to log the error. Again, if in the future, we decide to change how
we log the error we have to go back to several route handlers and make that change. So we want to move this logic for handling errors
to somewhere central. So in the future,
we want to make a change in the logic
to handle errors. There is a single place
we need to modify. So let's go to index dot g. This is where we are registering our
middleware functions. In Express, we have
a special kind of middleware function
called error middleware. We register that
middleware function after all the existing
middleware functions. So here we call app that US and pass a middleware function
with three parameters, request response, and next. But we also add a fourth
argument here at the front. That's the exception or error that we catch somewhere
else in the application. Now in this function, we add all the logic for handling
errors in our application. So back to cars Js. I'm going to cut this logic
from here and paste it here. Now, back to cars
Js one more time. Here in this cache block, we want to pass control to our error handling
middleware function. So we add a new
parameter here, next. And as you know, we call
next to pass control to the next middleware function in the request processing pipeline. Here in the cache block, we call next and pass this
error as an argument. Now, because in index or Js, we registered this function after all the existing
middleware functions, when we call next,
we will end up here. And the error that we pass will be the first argument
to this function. Now with this new
implementation, we have a single place
to handle errors. So if you want to make
changes in the future, we only come back and
modify this function. In a real world application, the logic for logging
the exception or errors might be
several lines long. We don't want to add all the
details in index dot Js. So in index or Js, we just want to
do orchestration. We want to do a high
level arrangement. The details should be encapsulated
in a different module. So I'm going to
move this function, this middleware function
to a separate module. So back here, we have
this middleware folder. Let's add a new file
here, error dot Js. Coming back to index Js, I'm going to get this function
here and in error Js, set module exports
to this function. So now we separated the details of error handling in
a separate module, and this results in better
separation of concerns. In index or Js on the top, we need to load this module. So const error. We set this to require. Then we go one level up
to the middleware folder, and unload the error module. And finally, here, we call app dot g and pass our
error handling function. Note that I'm not
calling this function. I'm just passing a
reference to this function. So now our application
has a better design. However, in cast or Js, we have this try cache block, and as you can tell, we have to repeat this in every route
handler in our application. We have to add this cache error, and here we should call next. This is repetitive. So
in the next lecture, I'm going to show you how to
improve this implementation.
5. Eliminating Try-Catch Blocks: In this route handler, we have a try cache block
to handle errors. The problem is, we
will end up repeating this try cache block in
every single route handler. This adds a lot of
noise to our code and make it harder to focus on the actual logic for each route. It's just distracting. Ideally, we want to move this high level
error handling logic somewhere else to
a single function that we can reuse
across all routes. Let me show you how
we can do this. First, we will create a reusable function
called async middleware. This function will
serve as a template. It will include the tri
cache block we need, but we will make it flexible so it can adapt to
different route handlers. So the function will take a route handler as an
argument like this. Inside the tr block, we will call that handler. If anything goes wrong, the cache block will pass the error next
middleware function. This way, the only
part that changes for each route is a specific
logic in the handler. Everything else stays the same. Alright, let's define
this async middleware. So this function will
look something like this. It takes a route handler
as an argument inside, we wrap the handler
in a tr cache block. If the handler throws an error, the cache block calls
next with the exception. Now let's use this
middleware in our route. So instead of writing that tr cache block directly
in the route handler, we pass this function
to async middleware. This middleware will take care of the error
handling for us. So we don't need
this next parameter, the tr block, and
the cache block. So our route code
becomes much cleaner. Now, look at this ASN function, the anonymous function that we are passing as
a route handler. Eventually, we want to pass this ASN function as an
argument to this new function. Now, because handler is an ASN function, we
should await it. And because we have
used await here, we should also mark
this function as AS. Now there is a tiny issue
we need to address. When calling the handler
inside Async Middleware, we need to pass the
request response and next parameters. But
here's the catch. Those objects aren't defined anywhere in a sync middleware. So how do you get access
to these parameters? Well, to solve this, we need to understand
how express works. So here, when we define
a route router dot get, let's say new route. Here we pass a route
handler function which takes three parameters, request response, and next, and it goes to a code plot. So we don't call this
function our sales. Instead, we pass a
reference to this function, and Express takes care of calling it and passing
the required arguments. That is request response
and next at runtime. But in our current
implementation, we are directly calling
Async Middleware. That's not how Express
expects it to work. We need to make a
small change here. So we will turn this Async middleware function
into a factory function. So instead of
directly calling it, we'll make it return
new function. This returned
function will act as the actual route handler
that Express can call. So this new returned function will accept request response, and next as parameters. And inside, it will call
the original handler, we pass to async Middleware, passing along request
response and next. Now, when Express calls
this returned function, it will provide the
necessary objects. That is request
response and next, and everything will
work seamlessly. With the setup, we have moved the tricchblock into
async middleware. This means our route handlers
are now super clean. We are awaiting the
call to the handler, so we need to mark
the calling function, which is this function as async. And we no longer need to apply this async here
anymore because in this function in
Async middleware nowhere we are
awaiting a promise, we are simply returning
an async function. Okay? Finally, let's make
Async middleware reusable. So we will move it to a separate module here in
our middleware folder. Let's create a new
file called async Js. Let me cut the code for
Async Middleware from here and paste it here. Finally, we will export it
as module thought exports, and we set this to fun engine. Back here in Casta Js, simply load this
Async middle ware. So on the top, const
async middleware. We set this to require then the middleware
folder and async. And that's it. Now you can use this middleware in any route handler across your application. So with this approach,
your core becomes cleaner, more focused, and
easier to maintain.
6. Using Express Async Errors module: In the last lecture, we define this async
middleware function. Now, while this async
middleware function solves the problem of
repetitive tr cach blocks, the issue we have is that we have to remember
to call it every time. And this also makes our
cord a little bit noisy. So in this lecture, I'm going to show you a
different approach. We are going to
use an NPM module, and this module will monkey patch our route
handlers at runtime. So when we send a request
to this endpoint, that module will wrap
our route handler code inside something like this. Let me show you how that works. So open up a terminal and
install Express, async errors. Let's install this. Now,
let's go to index or Js We need to load this module when
the application starts. So we will have a require of express async errors.
That's all we need to do. Don't have to get the result
and store it in a constant. Now back in cars or JS, we can remove the call to Async Middleware and get back to our original route
handler implementation, which is far simpler
and cleaner. Similarly, I'm going to remove the second call
to Async middleware. And finally, on the top, I'm going to remove the
required statement as well. Now let's test this and make sure it is
functioning properly. So back in the terminal, I'm going to run our
Fairwheels application. Now, in postmen, I'm going
to get all the cars. So that endpoint is working. Now I'm going to stop Manga DB. So here in the Manga
Di B terminal, let's stop this with top
service name Manga DB then send another request to
the server to send again. This is going to
take a little while. All right, here's
the response we were expecting a 500 error
with this message. So this verifies that this
module that we installed properly moved control from a route handler to our
error handling function. So you can see using Express Async errors
is very, very easy. And this is my suggested
approach for handling async errors in express
route handlers. However, if this
module doesn't work for your application
for whatever reason, then you need to switch back to this other approach and use
Async middleware function.
7. Logging Errors: This is our error middleware. Now, as I told you before, in every enterprise application, we need to log the exceptions that are thrown in
the application. So later we come back,
look at the log and see what are the areas of the application that
we can improve. So in this lecture, I'm going to introduce you to a very popular logging
library called Winston. So here's the Winston on NPM. The current version is 3.17. And as you can see,
there have been over 13 million
weekly downloads. It's a very popular and
feature rich library. So let's get started. Here in the terminal, NPM
install Winston. Beautiful. Now, back
to the NPM page. Here you can see the
recommended way to use Winston is to
create your own logger. And below, we have a sample
code to get started. So I will just copy this code and modify it
according to our app. Copy all this and paste
it here in arrodt Js. On the top, we have
our Winston module. Then we have our logger, which is created using
this create logger method. It contains an object
with few properties, level, format, default
meta, and transports. So let me explain
how this works. First, we have log level. So level controls which
logs are to be processed. That means it filter
logs by severity. So severity of all
levels is assumed to be numerically ascending from most important to least important. That means if we have error, then it has a severity of zero and have the
highest priority. Similarly, we have one, info, STDP and verbs debug and silly with a
severity of six, and it has the least priority. So here we have set the
default level to info, meaning all messages of info, warn and error will be lagged. And if we set the
level to verbos, then all the messages of verbs and above levels
will be locked. Next, we have format. So a format is used to
customize how logs appear. That is a plain text, a JCN object or timestamped. And there are more formats, these are implemented
in log form, a separate module from Winston, and you can have a look at this module to see all
the formats available. Here we are going to use the JCNFmat and we don't
need this default beta. Now, this logger object has
an array of transports. So a transport is essentially a storage
device for our logs. So it defines where
the logs are sent. Winston comes with a
few core transports. They are Console for
logging messages on the Console pile and HTTP for calling an SDDB
endpoint for logging messages. There are also
plugins for Winston. There are other NPM
modules for logging messages in Mongo DB and COS DB, which is another popular
no SQL database. There's also a
plugin for logging messages in Redis and Loge, which is a very popular
log analysis and monitoring service for
enterprise applications. Now here we are going to use
two different transports, one for logging messages into a file and another for
logging to the console. So let me change
this one to console. And we don't need this
file name property here. Our logger is ready to use, and if you just
want to have a file of logs in JSON format, this much code is
sufficient to do so. However, there is
more to Winston. It's also possible to set a custom format and a custom level on each
transport separately. And any number of formats may be combined into a single format
using format dot combine. So I'm going to use
a few formats in our transports and then combine them for a
better experience. Let me show you how to do this. First of all, let's remove
this default level and format. Now on the console, I want to use colors to log levels based on
the custom mapping. For that, we can use colorize format and combine
it with simple format. So here, add a property format, and then we call the
combined method, the winston dot
format dot combine and simply pass the formats
that we want to combine. So winston dot
format dot ColorIE and winston dot
format dot Simple. We also want to give
it a level info. So as I mentioned before, all the messages of info
one and error levels will be logged to the console. That's it. Similarly, we can customize our
file transport. As you can see, custom level
is already set to error. Great. Here, we want to make the logs more readable by
adding current date and time. It can be done by combining
timestamp and JCN formats. So here, one more time,
I'm calling combine. And then pass wstin
dot forma dot Time STAMP and instin
dot fam dot JCN. By doing so, we have
added a timestamp to each error that may occur
inside the JSN object. Finally, we need to use this logger in our
error middleware. So we call logger dot log. Then we pass an object
with a property level, which we can set to error
or you can simply use the error helper method to log the error directly. Like this. Now, we just need to
pass the error message. So we set the message
property to err dot message. Now to demonstrate this, let's go to cars dot js. Here in the get cars route, I'm going to throw an error. So throw new error not
able to fetch cars. So let's imagine somewhere
in the application, an error is thrown with the
current implementation. Our error middleware will
catch that exception. It will log it using Winston and return the 500
error to the lien. So let's test this. I'm going
to run the application, Norman Beautiful. Now, back in postman, let's send a get request
to the cars endpoint. Alright. So here's our
internal server error. Now, if you look at the console, we have error because we used
Winston dot error method, and its color is red. And this is the error
message that we have thrown, not able to fetch cars. So this is the console transport which we have customized
on the logger. Now in our project, you can see here we have this new
file, error dot log. Here we have a JSON object
with a few properties level, which is error message is not able to fetch cars
and the timestamp. So in the future, you can
query the log file and perhaps extract only the
errors at a specific date. So this is a big picture. We simply call logger dot error or one of the helper methods. And depending on the transport
that we have configured, Winston will log
the given message. In the next lecture,
I'm going to show you how to log on Mongo DB.
8. Logging to MongoDB: All right. Now let me show you how to log messages
to Manga Deb. The logging to Mangaib is made pretty simple with
another NPM package, Winston Mangaib. So
let's install it. NPM install Winston Mangaib. The version 6.0 0.0. So make sure you have
the exact same version. Otherwise, what I'm going to show you will not
work in your system. Back in E Js In
the last lecture, we added customized file
and Console transports. Now we are going to add a
new Manga Divi transport. First, we need to go on the top. After we load Winston, we need to load Winston Manga DB require Winston Manga div. And here we don't care about what is exported
from this module. We just need to require it. Okay with this, we can come
here and call logger dot at new Winston Dot transports Manga B. We pass in options object. There are a few properties here that you can look
in the documentation. The one that you
need to set is DB. So we set this to the connection
string of our database, Manga DB holland 127.0 0.0 0.142 7017 fair wheels. Now in a real world scenario, you may want to separate your log from your
operational database. That's a decision that varies from one
environment to another. Here, we are going to use the same database for
logging our errors. All right, we are
done with this. Now, we don't need to
make any other changes. So next time there
is an error in the application because we
have another transport, Winston will automatically
store our error in Mongadib. So let's run the
application again. And back in Postman, send a request to
the cars endpoint. Now, let's take a look
at MongaV Compass. So here's our
fairwheel database. Let's refresh. We can see we
have a new collection log, and this is the
message we lacked. So here's the timestamp. The level is set to error. And here's the message,
not able to fetch cars. Now in the last lecture,
I talked about setting a custom level on each
transport separately. So here, maybe you only want to lock the
errors in Mongoib. You don't want to
store information messages or debug messages. If that's the case, here
in the options object, you also set the level
property to error. So with this, only error
messages will be logged. Now, as we discussed before, if you said this to info, because info level
has a priority of two and is third in
the logging level, only the error warning, and info messages
will be logged. Nothing beyond info will
be logged in Manga Divi.
9. Uncaught Exceptions: Now, this error middleware
that we have added here only catches errors that happen as a part of request
processing pipeline. So this is particular
to Express. If an error is thrown outside
the context of Express, this middleware will not be car. Let me show you. At the
bottom of this file, after the export, I'm going
to throw a new error. So throw new error, something failed during startup. So this error is thrown outside the context of
processing a request. It's outside the
context of Express. So now, when I run
this application, you will see that this error
will crash the process, and Winston will not be able
to store it in the log. To verify this, let me
go to our log file, delete everything here, save
now back in the terminal. Let's run our app. Norman, Okay, so you can see our app crashed, and here's our error. Something failed during startup. And if you look at the log file, you can see there
is nothing here. So if you deploy this
application to production, your application won't work, and there is no way for
you to know what went wrong unless you have access to the console
on the server. That's where Winston comes in. It allows you to catch and log uncut exceptions
effortlessly. So in this lecture, I'm
going to show you how to properly handle uncaught
exceptions in a knot process. This is at a higher level. It's not tied to express. Now, back in EOD or Js, here, when creating your logger, add an exception
handler's property and pass an array with a
transport instance to it. So exception handlers. We set it to an array where
we pass a transport instance, new Winston dot
transports dot file. It takes an object with
a property file name. And here we want to log the uncut exceptions
in a separate file. So let me call this
exceptions dot log. And that's it. Easy, right? Now, what if you have already
set up a logger and want to enable exception handling
later? That's no problem. You can use exceptions
dot handle method. Let me show you. So I will commend
this out and below, after we define our logger, we call logger dot exceptions dot handle and pass the transport instance
as we did before. So new transports dot file. File Name exceptions dot lag. Now this approach is great for flexibility as your
application evolves. Finally, we don't want our app to crash after logging
the uncut exception. By default, Winston exits the process after logging
uncaught exception. You can disable this
behavior by setting exit on error property to falls
when creating your logger. So here, after
exception handlers, we set exit on error to falls, or you can assign it
dynamically like this. Logger dot on error is false. And let me comment out this one. Now, back in the terminal, let's run this one more time. Note that this time the process did not terminate because we
caught the exception here. So the process terminates if you don't catch an exception. Okay? Now, let's take a look
at our exceptions log file. You can see our error message, something failed during startup. With Winston, you can easily
manage uncut exceptions and control our
applications behavior during unexpected errors. In the next lecture,
we're going to look at unhandled
promise rejections.
10. Unhandled Rejections: In the last lecture, we talked about handling
uncaught exceptions. So if there is an exception
in your application and you have not caught that exception
using a cache block, you can use this exception
handlers property of the logger to log it in
a file using Winston. Just like with
uncaught exceptions, Winston makes it easy to cache and log unhandled rejections. With the current setup, an
unhandled rejection will also be logged into our
exceptions dot Log file without
crashing the app. Let me show it to you.
So here in error dot Js, we are throwing an exception. Let's replace this with
a rejected promise. So constant promise. We set this to promise dot, reject and pass an error object with a message,
promise rejected. So imagine this
process represents the result of an
asynchronous operation like a call to a database or a
remote TDP service, and so on. So we have a rejected promise. And as I told you
before, with promises, we either call then, and then we should call catch to make sure to
handle rejections, or if we are using the
Async and await syntax, we await the promise, but we should put this
in a try cachblock to case the exceptions
or rejections. In this code, we have a promise, and I'm going to call then
pass a simple callback console dot log D. But I'm
not going to call cache. So we will have an
unhandled rejection. Now if you run the app Nomon
our app is working fine. And if you check the
exceptions log file, here's our unhandled
rejection promise rejected. So Winston automatically
caught this as uncaught exception and logged it on exceptions
dot log IL however, with Winston, you have a property rejection handlers to handle unhandled
rejections separately. Let me show you how to do it. Here you can enable rejection handling when
creating your logger. So just like we add
exception handlers, we can add rejection
handlers and pass an array of
transport incense to it. So rejection handlers,
set it to an array. Then we pass new Winston
dot transports dot pile. It takes an object
with file name. Now I want to log the
rejections separately. So I'm going to call it rejections dot log,
and that's it. This setup writes all
your unhandled rejections dedicated rejections
dot log file. Now, what if you
have already set up your logger and want to handle
promise rejections later? Just like exception handling, Winston provides the
rejections handle method. So let me comment
this out below. Just after we call
logger dot exceptions, we call logger dot
rejections dot handle and simply pass a
transport instance new Winston dot ransport dot file filename
reactions dot lag. This approach lets
you add a transport specifically for rejections even after your logger
is initialized. So let's test it back in the terminal node index
dot JS. All right. Look at this warning,
unhandled rejection. And here's our error. Now, if you look at the log, we have a new file
rejections dot log. Look, here's our unhandled
promise rejection, and this time, it is not logged in exceptions
dot log file. Now with the current
version of node, this unhandled promise rejection should terminate
the nod process. But as you can see, this
process is still running. We are connected to Mongo DB. This happened because of the exit on error
property in our logger, which we set to falls. So whether you are dealing with an uncaught exception or
an unhandled reaction, as a best practice, you should terminate
the node process. So you should exit here
because at this point, your process can be
in an unclean state. So as a best practice, we should terminate the
process and restart it to make sure we start
with a clean state. Now, you might ask, if we
terminate the process, how are we going to
restart it in production? Well, there are tools for that, which we call process managers. And in the future, we are
going to look at one of those. So I'm going to
modify this code, and we simply remove the exit on error property
because by default, Winston will exit after logging an uncaught exception or
an unhandled rejection. One question you might
have is whether you should log messages to a file or to
a database like Manga Div. There are different
opinions about this, but I personally
believe you should use both transports
because each transport has strengths and weaknesses. Manga Di B or other databases
is good for quering data. So if you want to create a client application
for querying your log, it's much easier to
query the data in Mongo DB as opposed to
a flat file like this. However, it is possible
that your Mongo DB server goes down or you cannot connect to it
for whatever reason. In that case, it's better use the file system because file
system is always available. In a production environment, unhandled promise rejections can go unnoticed without
proper handling. So this way, we created an audit trail of what
went wrong and why. And that's how you handle unhandled rejections
with Winston. Whether it's uncut exceptions
or promise rejections, Winston helps you to maintain visibility and reliability
in your application.
11. Extracting Routes: Alright, so here's the
code in index or Js. The main issue we have here is lack of separation
of concerns. There are so many
things happening here, and that's why we have
a large number of required statements on
the top of this module. Below that, you can see we
have some configuration code. After that, we have got
something completely different, which is all about connecting
to Mongo Di Ba database. Then we move on to setting up our routes in
various middleware. These are different concerns. They should not
be mixed together in one file or one module. In this module, we should only orchestrate these concerns. So the details of them should be moved to
different modules. For example, the details
of setting up routes or the details of connecting to Mongo DB database, they
should be separated. So in this lecture, we
are going to focus on extracting routes into
a separate module. So let's create a new
folder called initialize. Here, I'm going to add a
new file routes dot JS, and here we should
export a function. So module dot Exports. We set this to a function. Now, in this function,
I'm going to add all the code for setting up our routes and other middleware. So back in index or Js, I'm going to cut all the
code and move it here. So look at the
dependencies here. We have a dependency to
app object to express, all these routers,
like companies, customers, and so on. So back in index or Js
on the top on line 14, this is how we create
the app object. We should have a single instance of that in the
entire application. In other words, we don't
want to load Express and then call it to create an app
object in our new module. So we want to send
a reference to this app to this new module. So this function should
take app as an argument. Okay? Now, back in index or Js, here we have the app object. We can load our new module
that is initialized routes. This returns a function, so we call it and pass the
app object. That's it. It's all we have to do. Now
let's clean up this module. So all these routers
that we have imported here, like
companies, customers, and so on, all these
should be moved to our new module because we have not referenced them anywhere
else in the index module. So cut back here,
paste them on the top. We have added most
of the dependencies. We also need Express and
the error middleware. So we can load the Express
on the top, const Express. Require Express. Now for the error middleware, I'm going to take it out of
index or Js because this is the only place you're referencing this
middleware function. Here's our error middleware. Cut back in routes or Js, and let's add that here. Now, back to index or Js, you can see the
core in this module is already much shorter. We don't have so many
required statements anymore, and also the implementation
is a little bit cleaner. Now one last thing
back to routes module, we need to change the paths to these routers because
the routes folder is not inside the
initialized folder. So anywhere we have peri slash, I'm going to replace that
with period period slash. So here I have selected
these two characters. In VS code, we can enable
multi cursor editing. I'm holding down Control
and D on Windows. If you're using
Mac, the shortcut is probably Command D. So see, I'm selecting multiple instances and then we can replace
them all in one go. So period period. Done.
12. Extracting the Logging Logic: Here's our error Js middleware. In this lecture, we
are going to move all the code for setting up logging with different module. That is anything that is
related to Winston and handling rejected promises and
uncaught exceptions. So in the initialized folder, let's add a new file, logging dot js, and then
back in error dot js. Take all this code for
setting up Winston. Get it and move it to
logging dot s right here. Now, we must export this logger, so module dot Exports. And we want to name it Logger. So logger, we set
this to Logger. Now back to index dot js. I would also like to move this require statement for handling asynchronous
errors in Express. I would rather put this
in our logging module, which is all about handling
and logging errors. So let's cut it from here and paste it in the
logging dot js module. Now finally, we
need to go back to index dot js and load
the logging module. Require initializes Logging. Note that I put this first. So just in case we get an error
in loading other modules, to make sure to lock that error and
terminate the process. So we are done with this
refactoring as an exercise. I want you to move all
the code for dealing with database initialization to a separate module
called dbdt Js. You will have my solution
in the next lecture.
13. Extracting the Database Logic: Here in index dot JS, this is the only code we have for database
initialization. So here in the
initialized folder, let's add a new file, dw dot JS. And here we are going
to export a function. So module dot Exports. We set this to a function. And then move all the database initialization code right here. Now, I'm going to make
a few changes here. First of all, when we connect, I don't want to do
Consult at log. I would rather log this as an informative message
using Winston. So on the top, let's load our
logger to require Logging. And we need to
destructure logger, so Canst Cebrass logger. So why are we restructuring it? If you remember, in
the logging dot js, we have assigned
the logger object as a property of
module dot exports. So to access the
logger property, you need to destructure it. I did this on purpose
for a quick refresher. Now back to db dot js. As we have our logger now, we can replace this console that log with logger dot info. Also we should remove
this cache method because if we can't
connect to Mongo Di B, we want to log that exception
and terminate the process. With the current implementation, we are handling this
rejected promise right here, and all we are doing is displaying this message
on the console. So we are not logging this. We are not terminating
the process. I use this here specifically
for demonstration purposes, but with the new implementation,
we don't need this. So let's delete
this and finally, we need to import
mongoose on the top. I'm going to take this out
of indexed or Js module. So back in indexed
or Js on the top, I'm going to remove
this line for importing mongoose and then
put it right here. So here's our database module. You can see the code is
very clean, very short. We have a single responsibility. We don't have too many
things mixed up together. Finally, we need to load
this module in index or Js. So here, I'm going to call
require initialize DV. Here we get a function,
so we call it. Now let's verify that with
our current implementation. If we can't connect
to the database during the application
initialization, that exception will be logged and the process
will be terminated. So open a new terminal. I'm going to stop the
Mongo DB process with stop service name Mongo DV. And then back to our
terminal window, node, indexed or Js So here's
our unhandled rejection. The process is terminated. And if you look at
rejections dot log, we can see the rejection
is logged here. Beautiful. So that's why I told you we should remove
the cache method here and let our
global error handler deal with that rejected promise. Here's your next exercise. I want you to go back
to index or Js and extract all the code for dealing with configuration
to a separate module. So more specifically, I'm talking about these few
lines here where we look for essential
configuration settings during the application
initialization. You will see my solution next.
14. Extracting the Config Logic: All right. Let's start by adding a new file in the
initialized folder. So config dot js. Again, we export a function. Now, back in index or Js, we take all the code for logging the configuration settings
into this new module. So here we have a dependency
to this config module. So I'm going to get this from index dot js and load it
on the top of this module. Okay, so if we don't have
this configuration setting, we don't want to log
something on the console. Rather, we want to store this as a fatal
error in our log. So instead of doing a console dot error
and process dot exit, it's better to
throw an exception, and then our current
infrastructure will catch that exception, log it, and terminate
the process. So we throw a new error
and use the error message. Also, as a best practice, always throw error objects
instead of strings, even though you can do
that in JavaScript. Because when you throw
an error object, this stack trace will be
available for you to see later. If you throw a string
with the error message, you will not have
the stack trace. Okay, so that's another best
practice for you to know. Now finally, let's go back to index or Js and load
this new module. So require initialize,
flash config. It's a function, so we call it. Now, let's test it. Before that, I need to
start the Manga DV service. So back in the
terminal, let's run start service name Manga DV. Okay, now we have
to start afresh. So close all the terminals
and open a new terminal like this and run nod index dot JS. All right, so the
process is terminated, but nothing is logged
on the console. Although if you
look at this file, you can see why our
application crashed. Patel error, JWD secret
key is not defined. This is good for a
production environment. But if you give
this application to a new developer and they run it, they have no idea
what's going on. So in our current
implementation, we are using only
a file transport for uncaught exceptions. So we should add a
console transport as well to display uncaught
exceptions on the console. So new Winston dot transports
console. And that's it. Let's go back to the terminal and run the application
one more time. You can see the reason
our application failed is because we have not
defined JWT private key. Now I want to show
you something. So go back to index dot js. Look at the index dot JS file. We have only 13
lines of code here. Remember what we had before? I think we had 30 to 40 lines of code with really poor
separation of concerns. With this new refactoring, we're doing only one thing, setting up the application. The details of logging, the details of routes, the details of database, and other aspects are
delegated to other modules. This is a single responsibility
principle in practice. Although there is still
scope for refactoring, we can move this configuration
of joy and this code here, which is responsible
for handling the server setup
and starting it. So I will leave it for you as an assignment
of this section.