DEV Community

a-tonchev
a-tonchev

Posted on

Migrating from KOA to uWebSockets.js in less than 2 hours

When developing own software, everyone wants to be creative and to make the best, coolest and fastest software.

While creating the ShopSuy shop system of JUST-SELL.online we want to create the fastest online shop possible. And the fastest software demands the fastest development stack.

I was using KOA as backend API for our software, then one day I had an idea to use websockets to reach additional performance.

KOA do not support Websockets by itself, so I tried to research for the best websockets available, this is how I landed on uWebSockets.js (uws)

I made also some benchmark on my existing server compared to uws and uws was around 4 times faster than KOA.

So I have decided to migrate from KOA to uws.

Well said, but the things are not so easy. I needed to figure out a way to reuse my KOA ctx and to adjust the routing. After a lot of thinking and reading I came to a solution, which implementation costs me less than 2 hours.

Okay lets start:

In KOA I had something like this:

const app = new Koa();
const router = new KoaRouter();

// CORS Setup
app.use(cors({
  credentials: true,
}));

// Setup some custom services and DB
app.use(async (ctx, next) => {
    await doSomething();
    try {
      await next();
    } finally {
      cleanup();
    }
});

// Setup another function
app.use(doSomethingElse);

// Thus I had my routes in KOA:
const routes = [
 {
   method: 'get',
   path: '/', 
   steps: [
     ctx => { ctx.body = 'Hello API!'; }, 
   ] 
 }, 
 { 
   method: 'post',
   path: '/users', 
   steps: [
     async (ctx, next) => { await doStepOne(); return next(); }, 
     async (ctx, next) => { await doStepTwo(); return next(); },
     async ctx => {
        const users = await getUsersFromDB();
        if(!users) { 
         ctx.body = 'Not Found'; 
         ctx.status = 401; 
         return; 
        }
        ctx.body = { users };
        return ctx.body;
     }
   ] 
 }, 
];

// Setup routes

routes.forEach(route => {
    router[route.method](route.path, ...route.steps);
})

app.use(router.routes());
app.use(router.allowedMethods());

const port = Number(process.env.PORT || 8080);
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

Enter fullscreen mode Exit fullscreen mode

Okay, until here is clear, now lets try to do the same with uws:

First thing would be to create custom ctx object, that will include all the functions you need in your KOA project.
This will do the setupContext function, which receives the uws req, res and the path of the route:

// If you want to use get params
const getParameters = (req, res, path) => {
  const params = {};

  if (req.getMethod() !== 'get' || !path.includes('/:')) return params;

  let paramsIndex = 0;

  for (const name of path.split('/')) {
    if (name[0] === ':') {
      params[name.substring(1)] = req.getParameter(paramsIndex);
      paramsIndex++;
    }
  }

  return params;
};

// And here we create our ctx
const setupContext = async (req, res, path) => {
  const queryString = req.getQuery();

  const ctx = ({
    state: {},
    body: '',
    params: getParameters(req, res, path),
    executeAfterFinish: [],
    request: {
      header: {},
      body: {},
      url: req.getUrl(),
      query: queryString ? qsToObject(queryString) : {},
    },
    userAgent: {
      _agent: {},
    },
  });

  // This function we need to simulate the finally calls 
  ctx.addToExecuteOnFinish = newFunction => ctx.executeAfterFinish.unshift(newFunction);

  // Setup Headers
  req.forEach((k, v) => {
    ctx.request.header[k] = v;
  });

  [ctx.request.body, ctx.request.rawBody] = await jsonParser(res) || [{}, ''];

  return ctx;
};
Enter fullscreen mode Exit fullscreen mode

The jsonParser function you can find here:

https://github.com/a-tonchev/rest-api-boilerplate/blob/master/startup/startupHelpers/jsonParser.js

Which is based on this issue: https://github.com/uNetworking/uWebSockets.js/discussions/64

After we have our ctx, we can use now the full solution for the migration:

const app = uWebSockets.App();

// Creating of our new routes 
// Don't forget to rename the method 'all' to 'any' if you have such

const routes = [
 {
   method: 'get',
   path: '/', 
   steps: [
     async ctx => { ctx.body = 'Hello API!'; }, 
   ] 
 }, 
 { 
   method: 'post',
   path: '/users', 
   steps: [
     async ctx => { await doStepOne(); }, 
     async ctx => { await doStepTwo(); },
     async ctx => {
        const users = await getUsersFromDB();
        if(!users) { 
         ctx.body = 'Not Found'; 
         ctx.status = 401; 
         return; 
        }
        ctx.body = { users };
     }
   ] 
 }, 
];

routes.forEach(route => {
    const { path, method, steps } = route;

    app[method](path, (res, req) => {

      // In uws we need to handle onAborted events:

      let isAborted = false;
      res.onAborted(() => {
        console.error('ABORTED!');
        isAborted = true;
      });

      try {
        (async () => {
          // Basically this is the place where we can simulate all our app.use 

          // Here we setup our ctx
          let ctx = await setupContext(req, res, path);

          // and here we start, don't forget to remove all next()
          await doSomething(ctx);

          // This is the finally step, we push it in an array that will be executed after our request finish
          ctx.addToExecuteOnFinish(cleanup);

          await doSomethingElse(ctx); // remove next() inside doSomethingElse

          // all the steps:

          try {
            for (const step of steps) {
              await step(ctx);
            }

            ctx.status = '200';
          } catch (err) {
            const {
              statusCode: status,
              body,
            } = err?.errorData || {};

            ctx.status = status || 500;

            ctx.body = body || {
              ok: false,
              data: {},
              code: 500,
            };

            onError(err, ctx); // this is your custom error handling
          }

          if (!isAborted) {
            /* we need to use res.cork because we return from await -> https://unetworking.github.io/uWebSockets.js/generated/interfaces/HttpResponse.html#cork */
            res.cork(() => {
              res.writeStatus(`${ctx.status || 400}`); // The status should be always string in uws!
              setupCors(res, ctx.request.header.origin); // The cors need to be set after status, else we will get always status 200
              res.end(
                typeof ctx.body === 'object' ? JSON.stringify(ctx.body) : ctx.body,
              );
            });
          }

          // Clean-up functions
          for (const functionToExecute of ctx.executeAfterFinish) {
            if (functionToExecute.constructor.name === 'AsyncFunction') {
              await functionToExecute();
            } else {
              functionToExecute();
            }
          }

          ctx = null; // And the final cleanup
        })();
      } catch (e) {
        console.error(e);
      }
    });
  });
});

const port = Number(process.env.PORT || 8080);
app.listen(port, listenSocket => {
  if (listenSocket) {
    console.info(`Server running on port ${port}`);
  }
});
Enter fullscreen mode Exit fullscreen mode

And this is the setupCors function:

const setupCors = (res, origin) => {
  res.writeHeader('Access-Control-Allow-Origin', `${origin}`)
    .writeHeader('Access-Control-Allow-Credentials', 'true')
    .writeHeader('Access-Control-Allow-Headers', 'origin, content-type, accept,'
      + ' x-requested-with, authorization, lang, domain-key')
    .writeHeader('Access-Control-Max-Age', '2592000')
    .writeHeader('Vary', 'Origin');
};
Enter fullscreen mode Exit fullscreen mode

What we did:

  1. We created our own ctx. Here you can rebuild easily by yourself everything that you need from your KOA project - userAgent, params, request, state etc...
  2. We adjusted the routes
  3. Move the app.use functions inside the route.forEach
  4. Use regular await function instead of app.use(function)

And you don't need to change anything else in your whole project!

Thats it, you can see my whole uws + mongodb boilerplate project here:

https://github.com/a-tonchev/rest-api-boilerplate

We use the same approach in our Shop SaaS, here the demo:

https://demo2.shopsuy.com

Best Regards
Anton Tonchev
JUST-SELL.online

Top comments (0)