Leapcell: The Best of Serverless Web Hosting
Fastify: A High-Performance Node.js Web Framework
Fastify is an efficient and fast Node.js web framework designed to provide optimal performance. Although it is relatively new, it has won the favor of many developers due to its high performance and low overhead. Fastify offers a concise development experience, supports fast routing and a plugin architecture, making it easy for developers to build and expand applications. Referring to the Fastify official website, the following will introduce Fastify from multiple aspects.
1. Main Features and Principles
- High Performance: Fastify is one of the fastest web frameworks. Depending on the code complexity, it can handle up to 30,000 requests per second.
- Scalability: With the help of hooks, plugins, and decorators, Fastify is fully scalable.
- Schema-Based: Although not mandatory, it is recommended to use JSON Schema to validate routes and serialize outputs. Fastify will compile the schema into high-performance functions.
- Logging: Choosing the best logger Pino almost eliminates the cost of logging.
- Developer-Friendly: This framework is highly expressive and convenient for developers' daily use without sacrificing performance and security.
- TypeScript-Ready: It strives to maintain TypeScript type declaration files to support the growing TypeScript community.
2. Background of Its Birth
Before Fastify emerged, Express and Koa were two widely used frameworks in the Node.js field. Although they are popular and easy to use, their performance is not optimal when handling a large number of requests. Fastify aims to solve the following key issues:
- Performance: By reducing the overhead of each request, it provides higher performance than existing frameworks and can handle more requests, which is crucial for building high-performance applications.
- Development Efficiency: With the help of the plugin architecture and out-of-the-box functions (such as schema validation, logging, etc.), it provides an efficient and easy-to-use development environment, accelerating the development process and reducing potential errors.
- Schema Validation: It has built-in support for JSON Schema, which not only helps to validate client input but also ensures the data consistency and reliability of the application. This is a function that many frameworks do not integrate at the core level.
- Scalability: The plugin architecture allows developers to easily add new functions or integrate third-party libraries without having a significant impact on the overall performance of the application.
By solving the above problems, Fastify provides a powerful platform for developing high-performance, maintainable, and easy-to-develop Node.js applications, and has quickly gained attention and usage in the JavaScript community.
3. Realization of High Performance
The key to Fastify's high performance lies in its design and architectural choices, which help to minimize the overhead of each request:
- Efficient Routing Distribution: Fastify adopts a fast routing distribution mechanism. When a request comes in, it can quickly determine the processing function to be called, greatly reducing the request processing time.
- Precompiled Serialization: Before sending a response, Fastify uses precompiled serialization functions instead of dynamically serializing objects at runtime, thus speeding up the serialization of responses and sending them to clients.
- Schema-Driven Development: Fastify strongly recommends (and in some cases requires) using JSON Schema to define the input and output of routes. This not only helps to validate and document the API but also allows Fastify to precompile the validation logic, improving runtime efficiency.
- Optimized Plugin System: Fastify's plugin architecture is designed efficiently, supporting code reuse and best practices while keeping the core framework lightweight, achieving a high degree of modularity and flexibility without sacrificing performance.
- Efficient Logging: The built-in logging tool Pino is designed for speed and provides logging functions with extremely low overhead, which is crucial for maintaining application performance.
- Zero-Cost Abstraction: Fastify's design philosophy is to minimize the performance overhead of the abstraction layer. Even when using various abstractions and convenient functions, it can maintain high performance.
Through these design decisions and optimizations, Fastify can provide excellent performance and is an ideal choice for building efficient and responsive Node.js applications.
4. Installation and Usage
4.1 Install Fastify via npm
npm install fastify
4.2 Simple Example Code
// Import the framework and instantiate it
import Fastify from "fastify";
const fastify = Fastify({
logger: true,
});
// Declare a route
fastify.get("/", async function handler(request, reply) {
return { hello: "world" };
});
// Run the server!
try {
await fastify.listen({ port: 3000 });
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
4.3 Generate a Skeleton Using the cli
npm install --global fastify-cli
fastify generate newproject
4.4 Example of Handling Requests with JSON Schema and Hooks
import Fastify from "fastify";
const fastify = Fastify({
logger: true,
});
fastify.route({
method: "GET",
url: "/",
schema: {
// request needs to have a querystring with a `name` parameter
querystring: {
type: "object",
properties: {
name: { type: "string" },
},
required: ["name"],
},
// the response needs to be an object with an `hello` property of type 'string'
response: {
200: {
type: "object",
properties: {
hello: { type: "string" },
},
},
},
},
// this function is executed for every request before the handler is executed
preHandler: async (request, reply) => {
// E.g. check authentication
},
handler: async (request, reply) => {
return { hello: "world" };
},
});
try {
await fastify.listen({ port: 3000 });
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
5. Plugins, Decorators, Middleware, Hooks
In Fastify, plugins, decorators, middleware, and hooks are core concepts of the framework, each playing a different role:
5.1 Plugins
Plugins are the main way to add functions, share code, or encapsulate logic in a Fastify application. A plugin can be a function that takes a Fastify instance, options, and a callback function as parameters. Plugins can register routes, add decorators, declare new hooks, and even encapsulate other plugins, and are used to build modular applications. Developers can use them to build reusable logic blocks and share them in different Fastify applications or different parts of the same application.
5.2 Decorators
Decorators are used to extend the Fastify instance, request (Request), and reply (Reply) objects. By adding new methods or properties, developers can add custom functions or data and make them available in different parts of the application. For example, a decorator can be added to add a method for each request to access shared configuration data or services.
5.3 Middleware
Although Fastify is not designed to rely on middleware, it supports the use of Express/Connect-style middleware, mainly for compatibility or integration of specific functions. Middleware can access the request and response objects, execute code, modify the request and response objects, terminate the request processing chain, or call the next middleware in the call stack. When using Fastify middleware, caution is required, as improper use may bypass some optimizations of Fastify and affect performance.
5.4 Hooks
Hooks are a mechanism in Fastify that allows developers to intervene and execute logic at different stages of the request lifecycle (such as after the request is received, before the route is resolved, before the response is sent, etc.). Hooks can be used to execute preprocessing or postprocessing logic, such as permission checking, request logging, modifying responses, etc. Fastify provides various types of hooks (such as onRequest, preHandler, onSend, etc.), allowing developers to finely control different stages of request processing.
These components work together to provide Fastify with great flexibility and scalability while maintaining the high-performance characteristics of the framework.
5.5 Lifecycle Weight
Components in Fastify (plugins, decorators, middleware, and hooks) follow a certain execution order and priority, which determines their timing of action in the request processing flow. Understanding this execution flow is crucial for designing efficient and reliable Fastify applications:
- Plugins: Loaded when the application starts and executed in the order of registration. After the application starts, the plugin settings are solidified, and the functions defined in the plugins (such as routes, hooks, decorators) can be used for each subsequent request.
- Decorators: There is no clear execution time for them. The methods or properties added to the decorated objects (Fastify instance, request, reply) are immediately available and remain valid throughout the life cycle of the objects.
- Middleware: Executed early in the processing flow of each request, specifically before route matching. Middleware can modify the request and reply objects or decide whether to pass the request to the next processor.
-
Hooks: Follow a specific execution order, which is reflected in the request processing flow:
- onRequest: Executed immediately after the request is received, before any other processing.
- preParsing: Executed before the request body is parsed.
- preValidation: Executed before route-level validation.
- preHandler: Executed before the route processing function.
- preSerialization: Executed before the response is serialized, before sending it to the client.
- onSend: Executed before the response is sent to the client, but after serialization.
- onResponse: Executed after the response is completely sent to the client.
Ability to Interrupt
- Plugins: Do not directly participate in the interruption of request processing but can register hooks or middleware that affect the process.
- Decorators: Do not control the process, so they do not participate in interruptions.
- Middleware: Can interrupt the request processing flow. For example, not calling next() or sending a response can stop the subsequent processing.
- Hooks: Specific hooks (such as preHandler) can decide whether to continue processing the request or directly send a response, thus interrupting the subsequent process.
Understanding the execution order and interruption capabilities of these components is crucial for building Fastify applications that behave as expected.
6. Lifecycle
Normal Process
- [Incoming Request]: A new request enters the system.
- ↓
- [Routing]: Determine the route corresponding to the request.
- ↓
- [Instance Logger]: Record the instance logs related to the request.
- ↓
-
[onRequest Hook]: Execute the
onRequest
hook function, which can be used for unified request preprocessing. - ↓
- [preParsing Hook]: Execute the hook function before the request body is parsed.
- ↓
- [Parsing]: Parse the request body.
- ↓
- [preValidation Hook]: Execute the hook function before route validation.
- ↓
-
{Validation}: Perform route validation:
- If the validation fails→[400]: Return a 400 error response.
- If the validation passes→↓
- [preHandler Hook]: Execute the hook function before the route processing function, and operations such as permission checking can be performed.
- ↓
- [User Handler]: Execute the user-defined route processing function.
- ↓
- [Reply]: Prepare the response content.
- ↓
- [preSerialization Hook]: Execute the hook function before the response is serialized.
- ↓
- [onSend Hook]: Execute the hook function before the response is sent.
- ↓
- [Outgoing Response]: Send the response.
- ↓
- [onResponse Hook]: Execute the hook function after the response is completely sent.
7. Encapsulation
Fastify's encapsulation context
is one of its basic features. The encapsulation context determines which decorators, registered hooks, and plugins can be used for routes. Each context only inherits from the parent context, and the parent context cannot access any entities in its descendant contexts. If the default situation does not meet the requirements, fastify-plugin can be used to break the encapsulation context, making the content registered in the descendant contexts available in the containing parent context.
8. JSON Schema Validation for Routes
JSON Schema is combined with routes in Fastify to validate client request data and format response data. By defining JSON Schema, it can be ensured that the incoming request data meets specific format and type requirements, and at the same time, control the structure of the response data, improving the robustness and security of the API.
Example Code
const fastify = require("fastify")();
// Define JSON Schema
const userSchema = {
type: "object",
required: ["username", "email", "gender"],
properties: {
username: { type: "string" },
email: { type: "string", format: "email" },
gender: { type: "string", minimum: "male" },
},
};
fastify.route({
method: "POST",
url: "/create-user",
schema: {
body: userSchema, // Use JSON Schema to validate the request body
response: {
200: {
type: "object",
properties: {
success: { type: "boolean" },
id: { type: "string" },
},
},
},
},
handler: (request, reply) => {
// Handle request logic
// Assume that creating a user is successful and return the user ID
reply.send({ success: true, id: "leapcell" });
},
});
fastify.listen(3000, (err) => {
if (err) throw err;
console.log("Server is running on http://localhost:3000");
});
In this example:
-
userSchema
is defined, describing the expected format of the request body, including required fields and the types of each field. - In the route configuration,
userSchema
is applied to the request body (body
) through theschema
property, and Fastify will automatically validate whether the incoming request data conforms to the defined schema. - The
schema
of the response is defined to ensure that the response data structure meets the expectations.
In this way, it can be ensured that all incoming requests are strictly validated and all responses conform to the预定格式, enhancing the robustness of the API and the predictability for users.
9. Examples
9.1 Hooks Handling Login Status Issues
const fastify = require("fastify")({ logger: true });
const secureSession = require("fastify-secure-session");
// Register the session plugin
fastify.register(secureSession, {
secret: "averylognsecretkey", // A long and complex secret key should be used
cookie: {
path: "/",
// Configure other cookie options as needed
},
});
// Apply the preHandler hook only to /api/a and /api/c
fastify.addHook("preHandler", (request, reply, done) => {
if (
(request.routerPath === "/api/1" || request.routerPath === "/api/2") &&
!request.session.user
) {
request.session.redirectTo = request.raw.url;
reply.redirect(302, "/login");
done();
} else {
done();
}
});
// Route that does not require session verification
fastify.get("/api/2", (req, reply) => {
reply.send({ message: "Access granted for /api/2" });
});
// Route that requires session verification
fastify.get("/api/1", (req, reply) => {
reply.send({ message: "Access granted for /api/1" });
});
fastify.get("/api/3", (req, reply) => {
reply.send({ message: "Access granted for /api/3" });
});
// Route for the login page
fastify.get("/login", (req, reply) => {
reply.send(`Login form goes here. POST to /login to authenticate.`);
});
// Login logic
fastify.post("/login", (req, reply) => {
const { username, password } = req.body;
if (username === "user" && password === "password") {
req.session.user = username;
const redirectTo = req.session.redirectTo || "/";
delete req.session.redirectTo;
reply.redirect(302, redirectTo);
} else {
reply.code(401).send({ error: "Unauthorized" });
}
});
// Start the server
fastify.listen(3000, (err) => {
if (err) {
fastify.log.error(err);
process.exit(1);
console.log(`Server listening on http://localhost:3000`);
});
9.2 Limiting Parameters
The following example requires that the a
and b
parameters must be passed in a post request:
const fastify = require("fastify")();
const postSchema = {
schema: {
body: {
type: "object",
required: ["a", "b"],
properties: {
a: { type: "string" },
b: { type: "string" }
}
},
response: {
// If missing, the default is to return a 400 error, and the return type of 400 can be modified here
400: {
type: "object",
properties: {
statusCode: { type: "number" },
error: { type: "string" },
message: { type: "string" }
}
}
},
// Here you can intercept the above default interception strategy
preValidation: (request, reply, done) => {
if (!request.body ||!request.body.a ||!request.body.b) {
reply
.code(400)
.send({ error: "Bad Request", message: "Missing required parameters" });
return;
}
done();
}
};
fastify.post("/api/data", postSchema, (request, reply) => {
// If it reaches here, it means that both the a and b parameters have passed the verification
reply.send({ message: "Success" });
});
fastify.listen(3000, (err) => {
if (err) {
console.error(err);
process.exit(1);
}
console.log("Server listening on port 3000");
});
If you do not define the preValidation
hook and the custom response schema
in the route, when the body
of a POST request does not conform to the defined JSON Schema, Fastify will, by default, return a 400 error (Bad Request). The response body of this error will contain information about which fields do not meet the requirements, but this information is automatically generated by Fastify and may not be as specific or clear as a custom error message. By default, if only the a
parameter is passed and the b
parameter is missing, Fastify will return a response containing error details, similar to:
{
"statusCode": 400,
"error": "Bad Request",
"message": "body should have required property 'b'"
}
Leapcell: The Best of Serverless Web Hosting
Finally, I recommend a platform that is most suitable for deploying NodeJS services: Leapcell
🚀 Build with Your Favorite Language
Develop effortlessly in JavaScript, Python, Go, or Rust.
🌍 Deploy Unlimited Projects for Free
Only pay for what you use—no requests, no charges.
⚡ Pay-as-You-Go, No Hidden Costs
No idle fees, just seamless scalability.
🔹 Follow us on Twitter: @LeapcellHQ
Top comments (0)