22
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!
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 };
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);
}
});
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.
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" });
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();
};
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 };
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();
}
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 :)
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: "[email protected]",
password: "password",
},
});
expect(response.statusCode).toBe(200);
expect(JSON.parse(response.body)).toHaveProperty("access_token");
});
Notice that I use the inject method from fastify, so I don't need to run a server in order to do some testing.
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!
22