Fastify is a high-performance web framework for Node.js, designed with a focus on speed, security, and a great developer experience. Inspired by frameworks like Hapi and Express, Fastify offers a modular and lightweight architecture, making it suitable for both small and large-scale applications. It includes built-in features such as efficient logging, JSON schema validation, and easy extensibility through a rich ecosystem of plugins, hooks, and decorators.
In this tutorial, you'll learn how to build a blog application using Fastify with SQLite3 as the database backend. We'll cover key Fastify concepts while implementing core blog functionalities: creating, reading, updating, and deleting articles.
Prerequisites
Before you start, make sure you have:
- The latest version of Node.js and npm installed on your system.
- A basic understanding of building web applications with JavaScript.
Step 1 — Understanding Fastify
Fastify is a modern web framework for Node.js, designed for speed, developer-friendliness, and robust security. Its performance excellence, marked by high throughput and low latency, is achieved through an intricately optimized HTTP layer. Fastify’s API is both concise and intuitive, catering to developers of all experience levels.
Key features of Fastify include:
- Schema-based Validation and Serialization: Fastify incorporates built-in, type-safe validation and data serialization using JSON Schema, ensuring reliability and high performance.
- Extensive Plugin Ecosystem: With over 260 plugins available, Fastify supports various tasks such as authentication, caching, and data integration. Its modular architecture also allows easy creation and integration of custom plugins.
- Efficient Logging: Fastify includes a highly efficient, low-overhead logger that delivers detailed insights without impacting performance.
- Extensible Lifecycle Hooks: Fastify offers fine-grained control over the request/response lifecycle through hooks, allowing customization of application behaviour at various stages.
The benchmarks below illustrate Fastify's performance compared to other popular Node.js frameworks, highlighting its capacity to handle a high number of requests per second:
These benchmarks showcase Fastify’s superior performance, although real-world results can vary depending on specific use cases and implementation details.
Step 2 — Setting up the project
In this section, you will set up the directory and install the necessary dependencies for the project.
To begin, clone the project repository on Github:
git clone https://github.com/betterstack-community/fastify-blog
Move into the directory:
cd fastify-blog
The directory is configured to use ES modules and includes EJS templates, CSS styles, and environment variable configurations.
Now, install the project dependencies, which include the Fastify web framework and the env-schema package for managing environment variables:
npm install
Next, create a .env
file with the following:
PORT=3000
LOG_LEVEL=info
NODE_ENV=development
DB_FILE=./blog.db
In the directory, you'll find src/app.js
, which contains the following:
import Fastify from "fastify";
import env from "./config/env.js";
import logger from "./config/logger.js";
const fastify = Fastify({
logger: logger,
});
fastify.get("/", function handler(request, reply) {
return { message: "Blog app demo" };
});
fastify.listen({ port: env.port }, (err, address) => {
if (err) {
fastify.log.error(err);
process.exit(1);
}
fastify.log.info(`Blog App is running in ${env.nodeEnv} mode at ${address}`);
});
In the src/app.js
file, Fastify is imported along with environment configurations from ./config/env.js
and a custom logger from ./config/logger.js
.
A Fastify instance is created with logging enabled. A route is defined at the root URL /
that returns a JSON message, "Blog app demo." The server is started using fastify.listen()
, which listens on the port specified in env.port
.
If an error occurs during startup, it is logged, and the process exits; otherwise, a log confirms the server is running, including the environment mode and address.
To run this server, execute the following command in your terminal:
npm run dev
The command starts the server using the dev
script defined in your
package.json
file. Once the server is up and running, you'll see output like
this:
> fastify-guide@1.0.0 dev
> node --watch src/app.js
{"level":"info","time":"2024-08-18T10:31:46.533Z","pid":30817,"host":"MACOOKs-MBP","msg":"Server listening at http://[::1]:3000"}
{"level":"info","time":"2024-08-18T10:31:46.534Z","pid":30817,"host":"MACOOKs-MBP","msg":"Server listening at http://127.0.0.1:3000"}
{"level":"info","time":"2024-08-18T10:31:46.534Z","pid":30817,"host":"MACOOKs-MBP","msg":"Blog App is running in development mode at http://[::1]:3000"}
This output confirms that the Fastify server has started and is listening for
connections on both IPv6 (http://[::1]:3000
) and IPv4
(http://127.0.0.1:3000
). To verify everything is working, open
http://127.0.0.1:3000
in your browser:
To see how Fastify automatically handles responses, open a second terminal and run the following command:
curl http://localhost:3000/ --include
The output will show:
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
content-length: 27
...
{"message":"Blog app demo"}%
Fastify automatically sets the correct Content-Type
header based on the data you return. For example, when returning a JSON object, Fastify sets the Content-Type
to application/json; charset=utf-8
. Unlike Express, where you typically need to explicitly set the content type with methods like res.json()
, Fastify handles this for you.
If you modify the route to return a plain text string:
...
fastify.get("/", function handler(request, reply) {
return "Blog app demo";
});
...
And run the curl
command again:
curl http://localhost:3000/ --include
The output will change to:
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
content-length: 13
Date: Sun, 18 Aug 2024 10:35:21 GMT
Connection: keep-alive
Keep-Alive: timeout=72
Blog app demo%
Fastify automatically adjusts the Content-Type
header to text/plain; charset=utf-8
, reflecting the change in response type. This automatic content negotiation simplifies your code and makes it more efficient by removing the need to manually set response headers, as you would in Express.
Next, you'll organize the code by separating it into routes and controllers. This structure will enhance maintainability and scalability, making it easier to manage and expand the application as it grows.
To begin, create the controllers
directory:
mkdir src/controllers
In your editor, create the root.controller.js
file with the following content:
export async function getRoot(request, reply) {
return "Blog app demo";
}
In this file, the getRoot()
function handles the logic for rendering the root
path of your application.
Next, create the routes
directory within the src
directory:
mkdir src/routes
Now, create the routes.js
file and add the code that follows:
import { getRoot } from "../controllers/root.controller.js";
export default async function routes(fastify, options) {
fastify.get("/", getRoot);
}
This file defines your application's routes, linking the root path /
to the getRoot()
controller.
Next, return to the app.js
file and remove the existing fastify.get("/")
route definition since it has been moved to a dedicated routes module:
const fastify = Fastify({
logger: logger,
});
// remove the following
fastify.get("/", function handler(request, reply) {
return "Blog app demo";
});
...
Now, add the following code to register the routes:
import Fastify from "fastify";
import env from "./config/env.js";
import logger from "./config/logger.js";
import routes from "./routes/routes.js";
...
await fastify.register(routes);
fastify.listen({ port: env.port }, (err, address) => {
...
});
With these changes, your code is better organized, making it easier to manage and extend. Now, refresh your browser to verify everything is working correctly:
With the project directory now set up, you can proceed to the next steps.
Step 3 — Creating the homepage
In this step, you'll set up a template engine in your Fastify application. While there are several templating engines available, this tutorial will focus on using EJS (Embedded JavaScript). This popular templating language allows you to embed JavaScript code within your HTML markup.
To use EJS, install the following necessary packages:
@fastify/view
: This plugin adds template rendering support to Fastify, allowing you to use various templating languages, including EJS.ejs
: The EJS templating language itself.
Install these packages by running:
npm install @fastify/view ejs
The starter files already include a views
directory inside the src
directory to store your EJS templates. The following templates are already in place:
layout.ejs
: Provides a uniform layout for your site, including meta tags, a title, header, and navigation. It dynamically inserts the page title and content using EJS.index.ejs
: If no posts are available, it shows a message encouraging the user to create a new post.
Here is the content of index.ejs
:
<p>There are no blog posts yet. Why not <a href="/post/new">create one</a>?</p>
Update the root.controller.js
file to render the homepage by returning the index.ejs
template with the title "Homepage":
export function getRoot(request, reply) {
return reply.view("index", { title: "Homepage"});
}
Next, add the highlighted code to set up the templating engine in the src/app.js
file:
...
import path from "node:path";
import fastifyView from "@fastify/view";
import ejs from "ejs";
const __dirname = import.meta.dirname;
const fastify = Fastify({
logger: logger,
});
await fastify.register(fastifyView, {
engine: {
ejs,
},
root: path.join(__dirname, "views"),
viewExt: "ejs",
layout: "layout.ejs",
});
await fastify.register(routes);
...
In this code, you register the @fastify/view
plugin, configure EJS as the template engine, specify the views
directory, and set layout.ejs
as the default layout. The root route handler now renders index.ejs
with reply.view()
, and the .ejs
extension is managed automatically.
After saving your changes, refresh http://127.0.0.1:3000/
to see the rendered template:
With the templates configured, the next step is to style the page.
Step 4 — Serving static files in Fastify
The layout.ejs
file references a styles.css
file inside the public
directory, but it won't be served by default. When developing your application, you'll often need to include static assets like images, JavaScript files, and styles. To ensure these files are served properly, you need to configure Fastify to handle static files.
Start by installing the @fastify/static
plugin with the following command:
npm install @fastify/static
Given that the public
directory already includes a CSS file with styles, you need to configure Fastify to serve these files by setting up the appropriate middleware. Add the following code to your src/app.js
file:
import Fastify from "fastify";
import fastifyView from "@fastify/view";
[higlight]
import fastifyStatic from "@fastify/static";
...
await fastify.register(fastifyView, {
...
});
fastify.register(fastifyStatic, {
root: path.join(__dirname, "public"),
prefix: "/public/",
});
...
In this code, you imported and registered the fastifyStatic
plugin, setting the public
directory as the root for static files and defining /public/
as the URL prefix. This configuration tells Fastify where to find and serve static assets.
After saving your changes, refresh your browser to see the updates:
You should now see the styles applied correctly.
Step 5 — Connecting to the database using Fastify
Now that your application is set up, it's time to incorporate a database. You'll use SQLite to manage your data, creating a reusable database connection throughout your application. This section will guide you through setting up the database, establishing the connection, and preparing the application to interact with the database.
First, install the necessary SQLite package:
npm install better-sqlite3
Next, create a db.js
file in the src/config
directory with the following
content:
import fp from "fastify-plugin";
import Database from "better-sqlite3";
import env from "./env.js";
async function dbConnector(fastify, options) {
const dbFile = env.dbFile || "./blog.db";
const db = new Database(dbFile, { verbose: console.log });
db.exec(`
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
content TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`);
fastify.decorate("db", db);
fastify.addHook("onClose", (fastify, done) => {
db.close();
done();
});
console.log("Database and posts table created successfully");
}
export default fp(dbConnector);
This code defines a Fastify plugin that connects to an SQLite database using the better-sqlite3
library. The dbConnector
function initializes the database from a file specified in the environment variables, defaulting to ./blog.db
if none is provided. It creates a posts
table if it doesn't already exist, ensuring the necessary schema is in place.
The line fastify.decorate("db", db);
makes the database connection accessible throughout the application as fastify.db
, allowing you to interact with the database in any route or plugin. Additionally, the plugin includes a cleanup hook to close the database connection when the server shuts down, ensuring a graceful shutdown process.
Now, register the database plugin in your app.js
file:
...
import logger from "./config/logger.js";
import routes from "./routes/routes.js";
import dbConnector from "./config/db.js";
...
fastify.register(dbConnector);
await fastify.register(routes);
...
Here, the database plugin dbConnector
is registered with Fastify. This ensures that the database connection is established when the server starts and accessible throughout your application via request.server
:
const post = await request.server.get("SELECT * FROM posts WHERE slug = ?", [
slug,
]);
This setup provides a clean, modular approach to database integration in your Fastify application. It separates database concerns from route logic and ensures proper initialization and cleanup of the database connection.
In the next section, you'll leverage this database connection to implement functionality for creating blog posts, building upon the solid foundation you've established.
Step 6 — Creating blog posts
With your database configured, you’re ready to implement the functionality for creating blog posts. This step involves installing a package for generating URL slugs, creating controller functions, and setting up a view template for the post-creation form.
Start by installing the slugify
package:
npm install slugify
This package generates URL-friendly slugs from post titles, enhancing SEO and making your blog post URLs more readable.
Next, create a createPost.controller.js
file in your controllers
directory:
import slugify from "slugify";
export function getNewPost(request, reply) {
return reply.view("new", { title: "Create New Post" });
}
export function createPost(request, reply) {
const { title, content } = request.body;
const slug = slugify(title, { lower: true, strict: true });
const { db } = request.server;
const insertStatement = db.prepare(
"INSERT INTO posts (title, slug, content) VALUES (?, ?, ?)"
);
insertStatement.run(title, slug, content);
return reply.redirect("/");
}
This file contains two essential functions: getNewPost
and createPost
. The
getNewPost
function renders the form for creating a new post, while
createPost
handles the form submission, creates a slug, inserts the new post
into the database, and redirects the user.
The use of slugify
ensures that your post URLs will be clean and consistent,
improving both user experience and search engine optimization. The strict
option in the slugify
function removes all characters that are not
alphanumeric or hyphens, further ensuring URL compatibility.
To complete the post creation flow, lets review the new.ejs
template in the views
directory:
<h1>Create New Post</h1>
<form class="create-post-form" action="/post" method="post">
<label for="title">Title:</label>
<input type="text" id="title" name="title" required />
<label for="content">Content:</label>
<textarea id="content" name="content" required></textarea>
<button type="submit">Create</button>
</form>
This template provides a straightforward form for creating new blog posts. When
the form submits to the /post
endpoint, your controller's createPost
function handles the submission.
Once the template is set up, you'll need to create a new route and corresponding controller for handling the creation of posts:
import { getRoot } from "../controllers/root.controller.js";
import {
getNewPost,
createPost,
} from "../controllers/createPost.controller.js";
export default async function routes(fastify, options) {
fastify.get("/", getRoot);
// Register post routes with the /post prefix
fastify.register(
async function (postRoutes) {
postRoutes.get("/new", getNewPost);
postRoutes.post("/", createPost);
},
{ prefix: "/post" }
);
}
The highlighted code imports the getNewPost
and createPost
functions from
the createPost.controller.js
file. It then registers these routes with a
/post
prefix, where the /new
route displays the form for creating a new
post, and the /
route handles the form submission to create the post.
To enable Fastify to handle form input, use the @fastify/formbody
plugin. First, install the package:
npm install @fastify/formbody
Now you can register the plugin in your app.js
file:
...
import fastifyView from "@fastify/view";
import fastifyStatic from "@fastify/static";
import fastifyFormbody from "@fastify/formbody";
...
await fastify.register(fastifyFormbody);
fastify.register(dbConnector);
...
This code adds form parsing capabilities to your Fastify application, allowing it to handle POST requests with form data.
When you visit http://localhost:3000/post/new
in your browser, you'll see the
form you created. You can now create a blog post with any content of your
choice:
After submitting the form, you’ll be redirected to the homepage. However, you won’t see the post listed yet, as the functionality to display posts on the homepage hasn't been implemented:
Currently, the setup allows users to submit posts with numeric or invalid inputs, which the database will accept without validation. We need to validate the input before storing it in the database to address this issue. This will ensure that only valid data is processed, improving the quality and integrity of the data in your application.
Step 7 — Validating Inputs with Fastify
Fastify has built-in support for input validation using Ajv, a high-performance JSON schema validator. This allows you to define schemas for query strings, body values, and outgoing data, ensuring that incoming data matches your expectations and automatically handling errors for invalid inputs.
Implementing input validation offers several benefits:
- Enhances security by preventing malformed or malicious data.
- Improves data integrity within your application.
- Reduces the need for manual error checking in route handlers.
- Automatically generates API documentation based on your schemas.
To validate incoming post data, enter the highlighted code below:
import slugify from "slugify";
import Ajv from "ajv";
const ajv = new Ajv();
// Define the schema for post data
const postSchema = {
type: "object",
properties: {
title: {
type: "string",
minLength: 1,
maxLength: 100,
pattern: "^(?=.*[a-zA-Z]).+$",
},
content: { type: "string", minLength: 1 },
},
required: ["title", "content"],
additionalProperties: false,
};
const validatePost = ajv.compile(postSchema);
export function getNewPost(request, reply) {
return reply.view("new", { title: "Create New Post" });
}
export function createPost(request, reply) {
const { title, content } = request.body;
// Validate the input
const valid = validatePost({ title, content });
if (!valid) {
return reply.status(400).send({
error: "Invalid input",
details: validatePost.errors,
});
}
const slug = slugify(title, { lower: true, strict: true });
const { db } = request.server;
const insertStatement = db.prepare(
"INSERT INTO posts (title, slug, content) VALUES (?, ?, ?)"
);
insertStatement.run(title, slug, content);
return reply.redirect("/");
}
The highlighted sections of the code are focused on validating the post data using the Ajv
library. First, Ajv
is imported and instantiated, and a schema is defined to specify the required structure and constraints for the title
and content
fields.
This schema is then compiled into a validation function. Later, in the createPost
function, this validation function checks whether the incoming data adheres to the schema. If the data is invalid, an error response with a status of 400
is returned, along with details about the validation errors. This ensures that only valid data is processed and stored.
After implementing this validation, you can test it by submitting invalid data (e.g., a numeric title) at http://localhost:3000/post/new
:
You'll receive a clear validation error, protecting your application from incorrect inputs and improving overall data integrity:
With this validation in place, your application is now safeguarded against invalid inputs. In the next section, you will learn how to list and display the posts you’ve created.
Step 8 — Listing the blog posts from the database
This section focuses on retrieving and displaying the blog posts you've created. The process involves updating the root controller to fetch posts from the database and modifying the index template to render these posts.
First, make the following changes to the root.controller.js
file:
export function getRoot(request, reply) {
const { db } = request.server;
const posts = db.prepare("SELECT * FROM posts").all();
return reply.view("index", { title: "Homepage", posts });
}
This updated code retrieves the database instance from the Fastify server,
executes a SQL query to fetch all posts, and passes the results to the
index.ejs
template.
Next, clear the contents and add the following code to the index.ejs
file:
<h1>Blog Posts</h1>
<% if (posts.length > 0) { %>
<ul class="posts">
<% posts.forEach(post => { %>
<li>
<a href="/post/<%= post.slug %>" class="posts-title"><%= post.title %></a>
<div class="post-actions">
<a href="/post/<%= post.slug %>/edit" class="btn">Edit</a>
<form
action="/post/<%= post.slug %>/delete"
method="post"
class="posts-form"
>
<button type="submit" class="btn">Delete</button>
</form>
</div>
</li>
<% }) %>
</ul>
<% } else { %>
<p>There are no blog posts yet. Why not <a href="/post/new">create one</a>?</p>
<% } %>
This template iterates over the posts
array and creates a list item for each post. Each post title is displayed as a link to the full post, with Edit and Delete buttons provided for each entry. If no posts are available, the template prompts the user to create a new one.
After saving these changes, reload http://localhost:3000/
in your browser:
You should now see a list of blog posts displayed on the homepage. This update lets your application dynamically render user-generated content, making it a functional blog.
While the Edit and Delete buttons are visible, their functionality has yet to be implemented. In future steps, these features will be added to complete your blog application's CRUD functionality.
Step 9 — Displaying full blog posts on dedicated pages
In this section, you'll implement a feature that allows users to view the full content of individual blog posts on dedicated pages.
First, create the getPost.controller.js
file:
export function getPost(request, reply) {
const { slug } = request.params;
const { db } = request.server;
const post = db.prepare("SELECT * FROM posts WHERE slug = ?").get(slug);
if (!post) {
return reply.status(404).send({ error: "Post not found" });
}
return reply.view("post", { title: post.title, post });
}
This controller function retrieves a blog post from the database using the slug
provided in the URL. If the post is found, it renders the post details using a
post.ejs
template. If no post is found with the given slug, it returns a 404
error, indicating it is unavailable.
Next, review the post.ejs
template in the views
directory:
<div class="post-content">
<h1><%= post.title %></h1>
<p><%= post.content %></p>
<a href="/">Back to Home</a>
</div>
This template displays a blog post's entire content, including its title and body. It also includes a "Back to Home" link for easy navigation back to the main list of posts.
Finally, add a route for viewing individual posts in your routes.js
file:
...
import { getPost } from "../controllers/getPost.controller.js";
export default async function routes(fastify, options) {
...
fastify.register(
async function (postRoutes) {
postRoutes.get("/new", getNewPost);
postRoutes.post("/", createPost);
postRoutes.get("/:slug", getPost);
},
{ prefix: "/post" }
);
}
...
This route uses the dynamic parameter :slug
to match the URL with the
corresponding blog post. When a user clicks on a post title or navigates to a
specific URL like , the getPost
controller fetches the relevant post and
renders it using the post.ejs
template.
With these changes, you can now click on the blog post title from the homepage to view its entire content on a dedicated page. Or you can navigate to http://localhost:3000/post/this-is-my-first-post
:
You will see that the blog post is being displayed in full.
In the following steps, you'll implement functionality for editing posts, allowing users to modify existing content.
Step 10 — Editing blog posts
In this section, you'll implement functionality to allow users to edit existing blog posts, enabling updates to both the title and content of their posts.
Create the editPost.controller.js
file:
import slugify from "slugify";
export function getEditPost(request, reply) {
const { slug } = request.params;
const { db } = request.server;
const post = db.prepare("SELECT * FROM posts WHERE slug = ?").get(slug);
if (!post) {
return reply.status(404).send({ error: "Post not found" });
}
return reply.view("edit", { title: "Edit Post", post });
}
export function editPost(request, reply) {
const { slug } = request.params;
const { title, content } = request.body;
const newSlug = slugify(title, { lower: true, strict: true });
const { db } = request.server;
const updateStatement = db.prepare(
"UPDATE posts SET title = ?, slug = ?, content = ? WHERE slug = ?"
);
updateStatement.run(title, newSlug, content, slug);
return reply.redirect(`/post/${newSlug}`);
}
This controller handles two main tasks: retrieving and rendering the post for
editing and processing updates. The getEditPost
function fetches the post
based on its slug, rendering an edit form, and returns a 404 error if the post
isn't found. The editPost
function processes the submitted form, updates the
post in the database with the new title, slug, and content, and then redirects
the user to the updated post page.
Next, review the edit.ejs
template in the views
directory:
<h1>Edit Post</h1>
<form class="edit-post-form" action="/post/<%= post.slug %>/edit" method="post">
<label for="title">Title:</label>
<input
type="text"
id="title"
name="title"
value="<%= post.title %>"
required
/>
<label for="content">Content:</label>
<textarea id="content" name="content" required><%= post.content %></textarea>
<button type="submit">Update</button>
</form>
This template displays a pre-filled form with the current post's title and content, allowing the user to edit it. When the form is submitted, it sends a POST request to update the post in the database.
Finally, add the necessary routes to routes.js
:
...
import { getPost } from "../controllers/getPost.controller.js";
import { getEditPost, editPost } from "../controllers/editPost.controller.js";
export default async function routes(fastify, options) {
fastify.get("/", getRoot);
fastify.register(
async function (postRoutes) {
...
postRoutes.get("/:slug/edit", getEditPost);
postRoutes.post("/:slug/edit", editPost);
},
{ prefix: "/post" }
);
}
These routes handle both the GET request to display the edit form and the POST request to process the form submission, updating the post in the database.
With these changes, you can now edit the blog post by either clicking the
Edit button on the homepage or navigating to
http://localhost:3000/post/this-is-my-first-post/edit
:
After updating and submitting the form, you'll be redirected to the updated post page:
The homepage will also reflect the changes:
This edit functionality completes another essential aspect of your blog application's CRUD operations. In the next section, you'll add the ability to delete posts, finalizing the core features needed for basic blog management.
Step 11 — Deleting blog posts
To complete the CRUD functionality, you'll often need the ability to delete blog posts. This section guides you through implementing the delete feature for your blog application.
To do that, create a deletePost.controller.js
file:
export function deletePost(request, reply) {
const { slug } = request.params;
const { db } = request.server;
const deleteStatement = db.prepare("DELETE FROM posts WHERE slug = ?");
deleteStatement.run(slug);
return reply.redirect("/");
}
This code defines a deletePost()
function that removes a blog post from the
database based on its slug. After the post is deleted, the user is redirected
back to the homepage.
Next, add the delete functionality to your routes by updating routes.js
:
...
import { deletePost } from "../controllers/deletePost.controller.js";
[/higlight]
export default async function routes(fastify, options) {
...
fastify.register(
async function (postRoutes) {
...
postRoutes.post("/:slug/edit", editPost);
postRoutes.post("/:slug/delete", deletePost);
},
{ prefix: "/post" }
);
}
This code adds the deletePost
route, which allows the deletion of blog posts
through a POST request to /post/:slug/delete
. This integrates the delete
functionality into the existing routing system.
Now, refresh the homepage at http://localhost:3000/
and click on the
delete button for the post:
Afterwards, the post will be removed from the database, and you will be redirected to the homepage, where the deleted post will no longer appear in the list:
With the delete functionality, your blog application fully supports the essential CRUD operations: Create, Read, Update, and Delete. This allows for comprehensive blog management, giving users complete control over their content.
Step 12 — Using Fastify hooks
Fastify’s middleware system, known as hooks, allows you to customize and enhance your application's request-response cycle. By defining custom hooks, you can intercept and modify requests and responses, perform additional operations, and add extra functionality to your application.
Fastify provides various hooks that execute at different stages of the request lifecycle:
- onRequest: Executes at the beginning of the request lifecycle before any parsing or validation.
- preParsing: Runs after the request is received but before it is parsed.
- preValidation: Executes after parsing but before validation, allowing you to modify the request before validation occurs.
- preHandler: Runs before the handler is executed, after validation is complete.
- onResponse: Executes after the response has been sent to the client, allowing you to perform actions post-response.
Each hook serves a specific purpose, allowing you to intervene at key points in the request processing pipeline.
To add middleware in Fastify, use the addHook
method. Below is an example of how to implement basic request logging:
...
const fastify = Fastify({
logger: logger,
});
fastify.addHook("onRequest", async (request, reply) => {
request.log.info(`Incoming request: ${request.method} ${request.url}`);
});
fastify.addHook("onResponse", async (request, reply) => {
request.log.info(
`Request completed: ${request.method} ${request.url} - Status ${reply.statusCode}`
);
});
...
In this example, the onRequest
hook logs incoming requests, capturing the HTTP method and URL before the request is processed. The onResponse
hook logs completed requests, including the HTTP method, URL, and status code after the response has been sent.
When you refresh the homepage at http://localhost:3000/
, you will see output like this in the console:
...
{"level":"info","time":"2024-08-18T16:14:36.380Z","pid":42449,"host":"ubuntu","reqId":"req-1","msg":"Incoming request: GET /"}
{"level":"info","time":"2024-08-18T16:14:36.385Z","pid":42449,"host":"ubuntu","reqId":"req-1","msg":"Request completed: GET / - Status 200"}
...
These hooks provide valuable insights into your application's request flow, enabling you to track and monitor incoming requests and their outcomes.
You can further customize these hooks or add more to implement advanced middleware features like authentication, rate limiting, or request transformation.
Step 13 — Error handling with Fastify
Effective error handling is essential for any application, ensuring graceful degradation, improving user experience, and aiding in debugging. Fastify excels with its built-in error handling, which logs detailed error information and keeps the application running, a feature particularly useful during development.
Consider this example where an error is introduced by referencing a non-existent database table:
export async function getRoot(request, reply) {
const { db } = request.server;
const posts = db.prepare("SELECT * FROM unkown_table").all();
return reply.view("index", { title: "Homepage", posts });
}
When you save and refresh the homepage, Fastify logs the error with comprehensive details:
{"level":"error","time":"2024-08-18T16:21:15.456Z","pid":42707,"host":"ubuntu","reqId":"req-1","req":{"method":"GET","url":"/","hostname":"localhost:3000","remoteAddress":"::1","remotePort":56796},"res":{"statusCode":500},"err":{"type":"SqliteError","message":"no such table: unkown_table","stack":"SqliteError: no such table: unkown_table\n at Database.prepare (/Users/stanley/fastify-blog/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)\n at Object.getRoot ... at Object.<anonymous> (/Users/stanley/d/fastify-blog/node_modules/@fastify/view/index.js:170:7)","code":"SQLITE_ERROR"},"msg":"no such table: unkown_table"}
..
In the output, Fastify automatically handles the error by logging the details and returning a 500 status code. This keeps the application running and provides useful information about where the error occurred.
If you refresh the homepage or run the following command:
curl http://localhost:3000/ --include
You will see the following error response:
[outptu]
HTTP/1.1 500 Internal Server Error
content-type: application/json; charset=utf-8
content-length: 112
Date: Sun, 18 Aug 2024 16:26:09 GMT
Connection: keep-alive
Keep-Alive: timeout=72
{"statusCode":500,"code":"SQLITE_ERROR","error":"Internal Server Error","message":"no such table: unkown_table"}%
Fastify automatically sets the correct status code and generates an error message, making error handling straightforward.
However, in production, you can customize this behavior to provide less detailed error messages to users. To do this, start by creating a middleware
directory in the src
directory:
mkdir src/middleware
Next, create an error.js
file in the middleware
directory with the following content:
import logger from "../config/logger.js";
function errorHandler(err, req, reply) {
let statusCode = 500;
let message = "internal server error";
if (err.code === "FST_ERR_VALIDATION") {
statusCode = 400;
message = "validation error";
logger.info(err);
} else {
logger.error(err);
}
const response = {
code: statusCode,
message,
};
reply.code(statusCode).send(response);
}
export default errorHandler;
This middleware captures unhandled errors, logs them appropriately, and sends a structured response to the client with a less detailed message, suitable for production environments.
Now, register the error-handling middleware in your routes.js
file:
...
import errorHandler from "../middleware/error.js";
export default async function routes(fastify, options) {
fastify.get("/", getRoot);
// Register post routes with the /post prefix
fastify.register(
...
);
fastify.setErrorHandler(errorHandler);
}
After implementing this, run the following command to test the error response:
curl http://localhost:3000/ --include
You should now see a response with a simplified error message:
HTTP/1.1 500 Internal Server Error
content-type: application/json; charset=utf-8
content-length: 46
...
{"code":500,"message":"internal server error"}%
With these changes, error messages are less detailed for end-users in production, improving security while still logging detailed information for debugging. You can now proceed to the next section to explore adding security headers to enhance your application's robustness further.
Step 14 — Enhancing application security and performance with Fastify plugins
In this section, you'll enhance your Fastify application by adding several key plugins to improve functionality, security, and performance.
Here is the overview of the plugins:
@fastify/cors
: Enables Cross-Origin Resource Sharing (CORS) for your application.@fastify/helmet
: Adds security-related HTTP headers to protect your application from common web vulnerabilities, such as XSS and clickjacking.@fastify/compress
: Compresses response bodies, which can significantly improve your application's performance by reducing the size of the data sent over the network.fastify-graceful-shutdown
: Ensures that your application shuts down gracefully, finishing ongoing requests before closing, which is essential for maintaining a good user experience during application restarts or shutdowns.
You can install these plugins using the following command:
npm install @fastify/cors @fastify/helmet @fastify/compress fastify-graceful-shutdown
After installation, import and register the plugins in your app.js
file:
...
import fastifyFormbody from "@fastify/formbody";
import fastifyCors from "@fastify/cors";
import fastifyHelmet from "@fastify/helmet";
import fastifyCompress from "@fastify/compress";
import fastifyGracefulShutdown from "fastify-graceful-shutdown";
...
const createServer = async () => {
...
await fastify.register(fastifyCors);
await fastify.register(fastifyHelmet);
await fastify.register(fastifyCompress);
await fastify.register(fastifyGracefulShutdown);
await fastify.register(fastifyFormbody);
fastify.register(dbConnector);
await fastify.register(routes);
return fastify;
};
export default createServer;
This code registers each plugin with the Fastify instance.
Verify that the plugins are working as expected with the following command:
curl http://localhost:3000/ --include
You should see output similar to the following:
HTTP/1.1 200 OK
Content-Security-Policy: default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-origin
Origin-Agent-Cluster: ?1
Referrer-Policy: no-referrer
Strict-Transport-Security: max-age=15552000; includeSubDomains
X-Content-Type-Options: nosniff
X-DNS-Prefetch-Control: off
X-Download-Options: noopen
X-Frame-Options: SAMEORIGIN
X-Permitted-Cross-Domain-Policies: none
X-XSS-Protection: 0
The output confirms that the fastifyHelmet
plugin is active, with essential
security headers such as Content-Security-Policy
and
Strict-Transport-Security
in place. These headers strengthen your
application's defense against common web vulnerabilities. Additionally, you'll
notice that the X-Powered-By
header has been removed as a security measure to
make your server less identifiable, reducing the risk of targeted attacks..
Though not visible in the curl output, the other plugins also function. The compressing plugin optimizes response sizes, and the Graceful Shutdown plugin ensures your application can close smoothly when needed.
Final thoughts
This article guides you through the basics of building a Fastify application, focusing on implementing full CRUD operations—creating, editing, deleting, and listing blog posts. It also covers essential concepts like reading environment variables, creating Fasify plugins, adding plugins for security and performance, and handling errors effectively. As a next step, explore the Fastify documentation for a deeper dive into its extensive features.
Make your mark
Join the writer's program
Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.
Write for usBuild on top of Better Stack
Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.
community@betterstack.comor submit a pull request and help us build better products for everyone.
See the full list of amazing projects on github