Middlewares in Koa

Middlewares in Koa

ยท

9 min read

What is a middleware?

A middleware in Koa is simply a function that takes two parameters: the Koa context object (ctx) and a function that represents the next middleware in the stack (next). Middlewares can modify the ctx object and control the flow of the middleware stack by calling next().

here's a very simple example of a middleware function in Koa:

async function myMiddleware(ctx, next) {
  console.log('This is my middleware');
  await next();
}

This middleware function simply logs a message to the console and then calls the next() function to proceed to the next middleware in the stack. You can add this middleware function to your Koa application using the app.use() method:

const Koa = require('koa');
const app = new Koa();

app.use(myMiddleware);

// ... other middleware and routes ...

In this example, we're creating a new Koa application and adding our myMiddleware function to the middleware stack using the app.use() method. When a request is made to your Koa application, the middleware function will be called and will log the message to the console before proceeding to the next middleware in the stack.

What is the middleware stack ?

The middleware stack in Koa is simply an array of middleware functions that are executed in sequence for each incoming HTTP request. When a request is made to your Koa application, Koa runs each middleware function in the stack in the order in which they were added using the app.use() method.

Each middleware function in the stack has the ability to modify the ctx object and/or control the flow of the middleware stack by calling the next() function. When a middleware function calls next(), Koa moves on to the next middleware function in the stack, until there are no more middleware functions to call.

In summary, the middleware stack in Koa is a way to chain together a series of middleware functions that can modify the ctx object and control the flow of the application's request-response cycle.

Here's an example of a simple middleware stack in Koa that logs a message to the console for each incoming request:

const Koa = require('koa');
const app = new Koa();

// Middleware function 1
app.use(async (ctx, next) => {
  console.log('Middleware 1');
  await next();
});

// Middleware function 2
app.use(async (ctx, next) => {
  console.log('Middleware 2');
  await next();
});

// Middleware function 3
app.use(async (ctx, next) => {
  console.log('Middleware 3');
  await next();
});

// ... other middleware and routes ...

// Start the server
app.listen(3000);
console.log('Server listening on port 3000');

In this example, we're creating a new Koa application and adding three middleware functions to the middleware stack using the app.use() method. Each middleware function logs a message to the console and then calls the next() function to proceed to the next middleware in the stack.

When a request is made to your Koa application, Koa will run each middleware function in the order in which they were added, logging a message to the console for each middleware function. Once all middleware functions have been executed, Koa will move on to the actual route handler or send a default response if no route matches the request.

You can see the middleware stack in action by running this example and making a request to your Koa application. You should see three messages logged to the console, one for each middleware function, before Koa sends a response back to the client.

Middleware Chain

A middleware chain is a sequence of middleware functions that are executed in a specific order by a web application framework like Koa. When a request is made to the application, it is passed through this chain of middleware functions one by one, and each function has the ability to modify the request or response objects or to pass control to the next middleware function in the chain using the next function. The order of middleware functions in the chain is important, as each function depends on the modifications made by the functions that came before it. The middleware chain ends when a middleware function does not call the next function, typically because it sends a response to the client or throws an error.

import Koa from "koa";

const app = new Koa();

const middlewareA = async (ctx, next) => {
  console.log("Middleware A");
  await next();
};

const middlewareB = async (ctx, next) => {
  console.log("Middleware B");
  await next();
};

const middlewareC = async (ctx, next) => {
  console.log("Middleware C");
  await next();
};

app.use(middlewareA);
app.use(middlewareB);
app.use(middlewareC);

What happens when next is not called in the middleware?

When next() is not called in a middleware function, the middleware chain will stop and subsequent middleware functions in the chain will not be executed.

In other words, any code that is meant to be executed by the subsequent middleware functions will be skipped. This can lead to unexpected behavior, such as incomplete or incorrect handling of the request.

In addition, if the middleware function is expected to modify the ctx object or perform some operation on the request or response, that operation will not be completed if next() is not called.

It's important to ensure that next() is called at the end of each middleware function to properly pass control to the next middleware in the chain.

Example:

const Koa = require('koa');
const app = new Koa();

// Middleware function 1
app.use((ctx, next) => {
  console.log('Middleware 1');
  // Call next to pass control to the next middleware function
  next();
});

// Middleware function 2
app.use((ctx, next) => {
  console.log('Middleware 2');
  // Call next to pass control to the next middleware function
  next();
});

// Middleware function 3
app.use((ctx, next) => {
  console.log('Middleware 3');
});

// Start the server
app.listen(3000);

In this example, we have three middleware functions that are defined using the app.use() method. Each middleware function logs a message to the console and then calls next() to pass control to the next middleware function in the chain.

If we start the server and make a request to it, we should see the following output in the console:

Middleware 1
Middleware 2
Middleware 3

Now, let's modify the example to remove the call to next() in Middleware function 2:

const Koa = require('koa');
const app = new Koa();

// Middleware function 1
app.use((ctx, next) => {
  console.log('Middleware 1');
  // Call next to pass control to the next middleware function
  next();
});

// Middleware function 2
app.use((ctx, next) => {
  console.log('Middleware 2');
  // Do not call next, which will prevent Middleware 3 from being executed
});

// Middleware function 3
app.use((ctx, next) => {
  console.log('Middleware 3');
});

// Start the server
app.listen(3000);

If we start the server and make a request to it, we should see the following output in the console:

Middleware 1
Middleware 2

Notice that Middleware 3 is not logged to the console. This is because next() was not called in Middleware 2, which prevented control from being passed to Middleware 3. This could lead to unexpected behavior and errors in your application, so it's important to always call next() in your middleware functions.

What happen if a middleware throw a error?

When a middleware throws an error, it stops the middleware chain and the error is propagated down to the next registered error-handling middleware. If there are no error-handling middleware functions registered, the error will propagate up to the top level of the application, which can cause the application to crash or produce an undesired behavior. Therefore, it is important to always have a global error-handling middleware that can handle any uncaught errors and prevent the application from crashing.

Here's an example:

Let's say you have two middleware functions, middleware1 and middleware2, and a route handler function, routeHandler. The middleware functions are added to the middleware stack in the order they are defined:

app.use(middleware1);
app.use(middleware2);

When a request is made to the server, the middleware functions will be executed in order, with each middleware calling await next() to move to the next middleware in the stack. If middleware2 throws an error, the middleware chain will stop executing and the error will not be handled by any remaining middleware or the route handler:

async function middleware1(ctx, next) {
  console.log('Middleware 1');
  await next();
}

async function middleware2(ctx, next) {
  console.log('Middleware 2');
  throw new Error('Something went wrong!');
}

async function routeHandler(ctx) {
  console.log('Route handler');
}

app.use(middleware1);
app.use(middleware2);
app.use(routeHandler);

In this example, if a request is made to the server and hits these middleware and route handler functions, the console output will be:

Middleware 1
Middleware 2

Because middleware2 threw an error and did not call next(), the middleware chain stopped executing and routeHandler was not called. The error was also not handled by any other middleware or the route handler.

Global Error Handler

A global error handling middleware is a middleware function that is responsible for catching errors that occur during the processing of a request and sending an appropriate response to the client. It's a centralized way of handling errors that occur in any middleware or route handler in your application.

Here is an example of a global error handling middleware in Koa:

import Koa from "koa";

const app = new Koa();

// middleware to catch errors
app.use(async (ctx, next) => {
  try {
    await next();
  } catch (err) {
    // set response status code
    ctx.status = err.status || 500;
    // set response body
    ctx.body = {
      error: {
        message: err.message,
      },
    };
    // emit the error event to the application
    ctx.app.emit("error", err, ctx);
  }
});

// route handler
app.use(async (ctx) => {
  // simulate an error
  throw new Error("Oops!");
});

// error event listener
app.on("error", (err) => {
  console.error("Server error", err);
});

app.listen(3000);

In this example, the first middleware function is responsible for catching errors that occur during the processing of a request. It does this by wrapping the next() function in a try-catch block. If an error is caught, it sets the response status code to the error's status code (or 500 if one is not set), sets the response body to an object containing an error message, and emits the error event to the application.

The second middleware function is a route handler that throws an error to simulate an error occurring during request processing.

Finally, the application listens for the error event and logs the error to the console.

This global error handling middleware allows for a centralized way of handling errors in your Koa application.

CONCLUSION

Today we covered the basics of middleware in Koa and the importance of calling next() to continue the middleware chain. We also discussed the potential issues that can arise when a middleware throws an error without calling next(), and the importance of having a global error handler to catch these errors.

ย