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 };
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.
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" });
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();
};
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 };
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();
}
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");
});
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)
How do you organize routes, controllers, services, schemas?