Node.js: Express API Microservices
NodeJS Docs
https://nodejs.org/
https://expressjs.com/
Official Node.js Docs https://nodejs.org/en/docs/guides/
Full Node.js Reference (for all core modules) https://nodejs.org/dist/latest/docs/api/
Node.js Event Loop https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/
Blocking and Non-Blocking Code https://nodejs.org/en/docs/guides/dont-block-the-event-loop/
Debugging Node.js https://nodejs.org/en/docs/guides/debugging-getting-started/
Debugging Node.js in VS Code
https://code.visualstudio.com/docs/nodejs/nodejs-debugging
https://www.loom.com/share/d9f0bf7f608643e987ac3de0378a074f
npm
and 3rd Party Packages
npm
and 3rd Party PackagesSee dedicated notes js_setup_n_deploy.md
Nodemon
Nodemon
See dedicated notes js_setup_n_deploy.md
Express Docs
https://expressjs.com/
Express.js
, aka Express
Express.js
, aka Express
Back end web application framework for building RESTful APIs with Node.js
.
The de facto standard server framework
for Node.js
.
Free and open-source
software under the MIT License.
Microservices: Problem Statement 1
As a project grows larger, we're adding more teams of 5-6 people work on the code base.
It quickly becomes unmanageable. How can we make this work?
Microservices: Solution 1
We can split up the code base in different services.
Each service is a different code base and belongs to a dedicated team.
Best practice is to also split the database, each service get its own db.
Services being independent means they are also much more reliable/available.
Microservices: Problem Statement 2
As a project gets traffic peaks on a specific part of the project which results in issues i.e. unable to handle the load/requests, API becoming unavailable temporarily because of too many requests
Microservices: Solution 2
Migrate system to a set of microservices with the objective to create a more robust and scalable solution that could handle traffic peaks, with the API always staying available.
Architecture: Problem Statement
Each service may have a different code base and database, but there might be inter-dependencies in how these services work.
How can we reduce these dependencies ?
Architecture: Solution
Highest dependency architecture is synchronous
. Services make direct calls to one another.
Lowest dependency architecture is asynchronous
. Services cannot make direct calls to one another, but only to an Event Broker
aka Event Bus
.
Asynchronous Communication
Pro: Query Service has zero dependencies on other services
Pro: Query Service will be extremely fast
Con: Data Duplication
Con: Harder to understand
Docker
Container = instance of an image, it runs a program.
Image = single file with all the dependencies and configurations required to run a program.
Express Install
npm i express
Express Usage
Always call next()
unless you send a response (with res()
).
If you send a response
(with res()
), never call next()
.
Body Parser Middleware
Used to process data sent in an HTTP request body
.
Provides four express middleware for parsing:
JSON
Text
URL-encoded
Raw data
Before parsing, HTTP request body
was just a regular string that could not access the data encoded inside.
After parsing, HTTP request body
becomes a JavaScript object
.
body-parser
allows you to access req.body
from within routes and use that data.
Body Parser Install
npm i body-parser
Handlebars Install
npm i express-handlebars@3.0
REST APIs
REST
= Representational State Transfer = Transfer Data instead of UI
JSON
= JavaScript Object Notation
API Endpoints
= combination of HTTP Method and Path
REST APIs are decoupled from the clients - there is no connection history
HTTP Request, HTTP Response, Headers
Request Methods, Response Status Codes, and more
Read note "js_api_calls"
REST Principles [Best Practice]
Uniform Interface
Clearly defined API endpoints with clearly defined request + response data structure.
Stateless Interactions
Server and client don't store any connection history, every request is handled separately.
Cacheable
Servers may set caching headers to allow the client to cache responses.
Client-Server
Server and client are separated, client is not concerned with persistent data storage.
Layered System
Server may forward requests to other APIs.
Code On Demand
Executable code may be transferred from server to client.
RESTful APIs Idempotent Methods [Best Practice]
HTTP methods that, when called multiple times with the same parameters, will produce the same result as a single call.
This ensures consistent behavior, especially when dealing with network issues or client retries.
Idempotent methods don't change the server's state beyond the initial operation.
CORS in Express [Best Practice]
CORS = Cross Origin Resource Sharing
Fix it in express:
app.use((req, res, next) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); next(); })
https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS
Debugging in Express
You might want to try to change:
http://localhost
forhttp://127.0.0.1
Firefox
forChrome
__dirname
in Node.js
__dirname
in Node.js__dirname
gives the directory in which the currently executing script resides.
__dirname
vs ./
https://stackoverflow.com/q/8131344/759452
https://nodejs.org/docs/latest/api/modules.html#__dirname
Error Handling in Express
Express
Express
comes with a default error handler
so you don’t need to write your own to get started.
It’s important to ensure that Express
catches all errors
that occur while running route handlers
and middleware
.
https://expressjs.com/en/guide/error-handling.html
https://www.udemy.com/course/nodejs-the-complete-guide/learn/lecture/12097888
Problem Statement for Error Handling in Express
When an error happen in the backend project:
should it be handled in the backend project?
if handled in the backend project, which layer should catch it? router, controller, or other?
what seems specific to
nodejs
/express
: is the above question related to the usage ofthrow
vsnext()
?if handled in the backend project, let's say we log the error - is it best practice to also pass the error to the frontend project in the response?
in Nodejs/Express backend projects, how do we let errors "bubble up"? i.e. we rethrow them? -or- we do not catch them? -or- we use
next()
? -or- else?
Solutions
If error originates in the backend, the error should be handled in the backend i.e. when validating requests, accessing databases. If something fails, it should catch and handle the error before sending a controlled response to the frontend i.e. error message with appropriate status code. Letting raw errors leak to the frontend is insecure and bad practice.
Ideally, the error is caught centrally in error-handling middleware. Controllers or routers can throw the error or pass it via
next(err)
, but don’t handle the response directly there unless absolutely necessary. A centralized error-handling middleware placed at the end of the middleware stack should log the error, determine the HTTP status code, and send a user-friendly error response. This separation is best practice and keeps controller logic clean.In Express, this is very specific. Unlike many frameworks that automatically catch throw, Express requires you to use next(err) if you want to pass an error to the centralized error handler. If you just throw inside an async function without a wrapper, it won’t be caught. So best practice is to use next(err), or wrap async functions to automatically catch errors and forward them to next().
Partially. Yes, you should send an error response to the frontend, but it should be sanitized. Never send internal error messages or stack traces. Instead, send a generic message (like "Something went wrong") and a proper HTTP status code (400 for bad input, 500 for internal errors, etc.). You can log detailed info (with stack trace) on the server, but only send user-friendly messages to the frontend.
The thing is that you let errors bubble up by passing them to the next layer using next(err). You don’t rethrow and you don’t catch them in every controller manually. If you're using async/await, you either wrap your async functions in a try-catch and call next(err) in the catch block, or use a utility wrapper (like express-async-errors or a custom catchAsync function). This way, the error will reach the centralized error-handling middleware and you won’t repeat error handling in each route/controller.
Synchronous Errors
Errors that occur in synchronous code inside route handlers and middleware require no extra work.
If synchronous code throws an error, then Express will catch and process it.
"Catch and process" synchronous errors refers to how it handles those errors through its built-in error-handling middleware system.
How Express "Processes" Errors
When a synchronous error is thrown inside a route handler or middleware, Express does the following:
Catches the error using try-catch logic internally (you don't see it, but it's there in the framework).
Skips the remaining middleware/handlers for that route.
Passes the error to the first error-handling middleware it finds in the middleware stack.
What Is an Error-Handling Middleware?
It's a special kind of middleware with four parameters, specifically:
(err, req, res, next) => { ... }
Example:
app.use((err, req, res, next) => { console.error(err.stack); res.status(500).send('Something broke!'); });
As long as you define this kind of middleware in your app, Express will "process" errors by passing them to this handler.
Asynchronous Errors
Errors inside setTimeout
, Promises
, or async/await
.
They need to be passed to next(err)
manually (where Express will catch and process them), or caught with try/catch.
For example:
app.get('/', (req, res, next) => { fs.readFile('/file-does-not-exist', (err, data) => { if (err) { next(err) // Pass errors to Express. } else { res.send(data) } }) })
next()
next()
next('route') to bypass the remaining route callback(s)
Express: Route Handler vs Middleware Function
Middleware can be a route handler or a route handler can behave as middleware. There is not a hard and fast line between the two.
But, when people refer to middleware or a route handler in a programming discussion, they usually mean something slightly different for each...
Middleware Function - as generic terms, middleware is code that examines an incoming request and prepares it for further processing by other handlers or short circuits the processing (like when it discovered the user is not authenticated yet). Some examples:
Session Management
Authentication
Metrics
Parsing of Cookies
Parsing and Reading of POST/PUT bodies
Serve static files (HTML, CSS, JS, etc...)
It is common for middleware to be active for a large number of different requests.
Route Handler - code that is looking for a request to a specific incoming URL such as /login
and often a specific HTTP verb such as POST and has specific code for handling that precise URL and verb. Some examples:
Serve a specific web page
Handle a specific form post
Respond to a specific API request
It is common for route handlers to be targeted at a specific URL and only active for that specific URL.
https://stackoverflow.com/a/58925330/759452
Express Route Handlers and Middleware Functions
app.use(path, handlerFc) app.get(path, handlerFc) app.post(path, handlerFc) app.put(path, handlerFc) app.delete(path, handlerFc) app.all(path, handlerFc)
Use them such as :
app.use('/', (req, res, next) => { /* code */ }); app.get('/something', (req, res, next) => { /* code */ }); app.post('/something', (req, res, next) => { /* code */ });
https://expressjs.com/en/5x/api.html#router
Express Routing: get()
get()
app.get()
is intended for specific routes.
app.get()
matches only for requests done with the GET
HTTP method.
app.get('/', handlerFc)
only matches '/', and does NOT match /hello
nor /hi/how/things
.
It is common for middleware to be active for a large number of different requests. Hence the above design.
Express Routing: post()
post()
app.post()
is intended for specific routes.
app.post()
matches only for requests done with the POST
HTTP method.
tl;dr: same as app.get()
but for POST
HTTP method.
Express: app.use()
app.use()
Middleware Functions aka Middleware Mounters.
app.use()
is intended for binding middleware to your application.
app.use()
matches regardless of the HTTP Method whether POST
, GET
, PUT
, DELETE
or other.
app.use()
matches any path that begins with the specified path. For instance app.use('/', handlerFc)
matches all three URLs:
/
/hello
/hi/how/things
It is common for middleware to be active for a large number of different requests. Hence the above design.
Express: app.use()
To Serve Static Files
app.use()
To Serve Static Filesapp.use('/static', express.static(__dirname + '/public'));
Express: app.use()
To Embed Subapp
app.use()
To Embed Subappapp.use('/subapp', require('./subapp'));
The above embeds another application.
Express: Request
Request aka req
object.
The req
object represents the HTTP request and has properties for the request query string, parameters, body, HTTP headers, and so on.
The req
object is an enhanced version of Node’s own request
object and supports all built-in fields and methods.
https://expressjs.com/en/5x/api.html#req
https://nodejs.org/api/http.html#class-httpclientrequest
Express: Response
Response aka res
object.
The res
object represents the HTTP response that an Express app sends when it gets an HTTP request.
The res object is an enhanced version of Node’s own response
object and supports all built-in fields and methods.
https://nodejs.org/api/http.html#class-httpserverresponse
Last updated