DEV Community

Cover image for How i structure my Fastify application
Renato Pozzi
Renato Pozzi

Posted on • Updated on

How i structure my Fastify application

Fastify is obviously a great choice to start with a REST API application, it's very simple to go up and running, it's full of already made and tested plugins, and finally, is also (as the name says) fast.

However, I noticed, and also tried it on my skin, that there is a common problem of structuring the application folder to have a solution that can scale, but without tons of directories.

So, I decided to write an article to share the configuration that I use on my Fastify projects. The goal is to give the reader some starting point for his app, this is not the 100% correct solution for all the projects, but a solution which in my case was correct.

So, let's get started!

First, app.js and server.js

The first thing that I do is split, the app initialization from the app entry point into two separate files, app.js and server.js, this became really helpful because you can have all your app routes and plugins initialized in a common build function in the app.js, and the app listening in the server.js.

This an example of app.js:

require("dotenv").config();

const fastify = require("fastify");
const cookie = require("fastify-cookie");

const { debug } = require("./routes/debug");
const { auth } = require("./routes/auth");
const { me } = require("./routes/me");

const build = (opts = {}) => {
  const app = fastify(opts);

  app.register(cookie);

  app.register(debug);
  app.register(me, { prefix: "/v2/me" });
  app.register(auth, { prefix: "/v2/auth" });

  return app;
};

module.exports = { build };
Enter fullscreen mode Exit fullscreen mode

An this an example of the server.js:

const { build } = require("./app.js");

const app = build({ logger: true });

app.listen(process.env.PORT || 5000, "0.0.0.0", (err, address) => {
  if (err) {
    console.log(err);
    process.exit(1);
  }
});
Enter fullscreen mode Exit fullscreen mode

As you can see, the app Is the returning object of the build function, so if I need it in another place (unit testing for example), I can simply import the build function.

Second, application routes

For the logic of the routes, I prefer to split all of them into separate files with the discriminant of logic. Probably you have noticed in the example before these rows:

app.register(debug);
app.register(me, { prefix: "/v2/me" });
app.register(auth, { prefix: "/v2/auth" });
Enter fullscreen mode Exit fullscreen mode

The idea here is, my app.js is the main reference, in this file I can see all the "macro" routes, and have some first impact logic flow. All the logic of all the single routes though are specified in its file.

This improves a lot the application code quality and also permits discrete scalability. Also, you can wrap some middleware like the JWT validation in a specific route file in order to apply the common logic to all the subroutes of the file.

An example of the me.js routes file:

const me = (fastify, _, done) => {
  fastify.addHook("onRequest", (request) => request.jwtVerify());

  fastify.get("/", getMe);
  fastify.put("/", putMeOpts, putMe);

  done();
};
Enter fullscreen mode Exit fullscreen mode

Third, lib and utils folders

There is always some quarrel between the purpose of the lib folder and that of the utils folder, now I tell you mine.

I use the utils folder principally for something very common, which I can use in every piece of code. You know, something like a sum functions, or some constants declaration, or maybe a hashing function, every piece of code which has a logic only for itself.

// ./utils/hash.js

const bcrypt = require("bcryptjs");

const hash = (plainText) => bcrypt.hashSync(plainText, 10);

const verify = (plainText, hashText) => bcrypt.compareSync(plainText, hashText);

module.exports = { hash, verify };
Enter fullscreen mode Exit fullscreen mode

The lib folder instead, it's the container for the app business logic, which is not "repeatable", something like the database factory, or the database queries.

// ./lib/db.js

export async function deleteWebsite(seed) {
  return new Website()
    .where("seed", seed)
    .destroy();
}
Enter fullscreen mode Exit fullscreen mode

Fourth, static files

For the static files is very simple, I use the fastify-static plugin, and I store all the public data in a public folder. Please don't use silly names :)

Fifth, unit testing

For the final point, all I need to do is to connect all the previous broken pieces and work with them, in my case, I usually do testing with Jest, but is quite the same with other frameworks.

Under every directory, I place a tests folder, and I name the files as the real application file, so me.js => me.test.js, and i recall at the build function on top of this article. Something like this:

  it("does login", async () => {
    const app = await build();
    const response = await app.inject({
      method: "POST",
      url: "/v2/auth/login",
      payload: {
        email: "info@renatopozzi.me",
        password: "password",
      },
    });

    expect(response.statusCode).toBe(200);
    expect(JSON.parse(response.body)).toHaveProperty("access_token");
  });
Enter fullscreen mode Exit fullscreen mode

Notice that I use the inject method from fastify, so I don't need to run a server in order to do some testing.

Wrapping up

So today we saw something quite common in the "microframeworks" world, the app structure, I hope this article has brought you some inspiration for your next projects!

If you are interested in learning more, I have created an open-source project in fastify, you can look at the sources from here if you are interested!

Hope to find you again soon!

While you're there, follow me on Twitter!

Top comments (1)

Collapse
 
minemaxua profile image
Maksym Minenko

How do you organize routes, controllers, services, schemas?