A crucial part of almost every system is a secure authentication mechanism. In this post, we'll implement authentication in a Node.js API built with Fastify. The creation of the API and the first routes related to the user table have already been covered in my previous posts — Check it out here. The base code is available in this GitHub repository: Blog - by micaelmi. Creating a Login Route To start the authentication process, we need to create a login route where the user will obtain an access token to use in protected routes. Inside src/routes/users, create login.ts: import bcrypt from "bcrypt"; import type { FastifyInstance } from "fastify"; import { ZodTypeProvider } from "fastify-type-provider-zod"; import jwt from "jsonwebtoken"; import z from "zod"; import { env } from "../../env"; import { ClientError } from "../../errors/client-error"; import { prisma } from "../../lib/prisma"; export async function login(app: FastifyInstance) { app.withTypeProvider().post( "/users/login", { schema: { summary: "User Login", tags: ["users"], body: z.object({ credential: z.string().min(4), // username or email password: z.string().min(8).max(32), }), }, }, async (request, reply) => { const { credential, password } = request.body; const user = await prisma.user.findFirst({ where: { OR: [{ username: credential }, { email: credential }], }, }); if (!user) throw new ClientError("User does not exist"); const passwordMatch = await bcrypt.compare(password, user.password); if (!passwordMatch) throw new ClientError("Password does not match"); const secretJwtKey = env.JWT_SECRET_KEY; const expirationTime = "30d"; const token = jwt.sign( { sub: user.id, name: user.name, username: user.username, type: user.userTypeId, }, secretJwtKey, { expiresIn: expirationTime } ); return reply.send({ token }); } ); } Explanation: Search for a user in the database using the provided username or email. Compare the provided password with the stored hashed password using bcrypt. Create a JWT token containing relevant user data from the database. Send the generated token back to the user. Now, register the route in server.ts: app.register(login); Test it in Swagger UI. The expected response should look like this: { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } You can inspect the token's payload on the JWT Website. Creating a Middleware to Verify Token Validity Now we need to create a middleware — a function that runs between the request and response cycle, processing requests before they reach the final handler. This middleware ensures that only authenticated users can access protected routes. import { FastifyRequest, FastifyReply } from "fastify"; import jwt, { JwtPayload } from "jsonwebtoken"; import { env } from "../env"; const secretKey = env.JWT_SECRET_KEY; export const verifyToken = async ( request: FastifyRequest, reply: FastifyReply ) => { try { const authorizationHeader = request.headers.authorization; if (!authorizationHeader) { return reply.status(401).send({ error: "Token not provided" }); } const token = authorizationHeader.split(" ")[1]; const decoded = jwt.verify(token, secretKey); if (typeof decoded === "string") { return reply.status(401).send({ error: "Invalid token" }); } request.user = decoded as JwtPayload & { sub: string; name: string; username: string; type: string; iat: number; exp: number; }; } catch (err: any) { console.error("Error on token validation:", err.message); return reply.status(401).send({ error: "Invalid token" }); } }; This function: Ensures a token is provided. Verifies the token's validity. Stores the decoded user data in request.user for later use. Applying the Middleware and Testing Route Protection To enforce authentication, add a preHandler hook in server.ts: app.register(async (app) => { app.addHook("preHandler", verifyToken); // Protected routes go here }); Now, test a protected route without a token. The response should be: { "error": "Token not provided" } To enable token authentication in Swagger UI, update fastifySwagger configuration, adding the following code right after the info object: //... info: { title: "Blog API", description: "API for my blog project.", version: "1.0.0", }, // ⬇️⬇️⬇️ securityDefinitions: { BearerAuth: { type: "apiKey", name: "Authorization", in: "header", description: "Enter your JWT token in the format: Bearer ", }, }, security: [{ BearerAuth: [] }], // ... A new "Authorize" button will appear. Click it and enter your token in the following format: Bea

A crucial part of almost every system is a secure authentication mechanism. In this post, we'll implement authentication in a Node.js API built with Fastify. The creation of the API and the first routes related to the user table have already been covered in my previous posts — Check it out here. The base code is available in this GitHub repository: Blog - by micaelmi.
Creating a Login Route
To start the authentication process, we need to create a login route where the user will obtain an access token to use in protected routes. Inside src/routes/users
, create login.ts
:
import bcrypt from "bcrypt";
import type { FastifyInstance } from "fastify";
import { ZodTypeProvider } from "fastify-type-provider-zod";
import jwt from "jsonwebtoken";
import z from "zod";
import { env } from "../../env";
import { ClientError } from "../../errors/client-error";
import { prisma } from "../../lib/prisma";
export async function login(app: FastifyInstance) {
app.withTypeProvider<ZodTypeProvider>().post(
"/users/login",
{
schema: {
summary: "User Login",
tags: ["users"],
body: z.object({
credential: z.string().min(4), // username or email
password: z.string().min(8).max(32),
}),
},
},
async (request, reply) => {
const { credential, password } = request.body;
const user = await prisma.user.findFirst({
where: {
OR: [{ username: credential }, { email: credential }],
},
});
if (!user) throw new ClientError("User does not exist");
const passwordMatch = await bcrypt.compare(password, user.password);
if (!passwordMatch) throw new ClientError("Password does not match");
const secretJwtKey = env.JWT_SECRET_KEY;
const expirationTime = "30d";
const token = jwt.sign(
{
sub: user.id,
name: user.name,
username: user.username,
type: user.userTypeId,
},
secretJwtKey,
{ expiresIn: expirationTime }
);
return reply.send({ token });
}
);
}
Explanation:
- Search for a user in the database using the provided username or email.
- Compare the provided password with the stored hashed password using bcrypt.
- Create a JWT token containing relevant user data from the database.
- Send the generated token back to the user.
Now, register the route in server.ts
:
app.register(login);
Test it in Swagger UI. The expected response should look like this:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
You can inspect the token's payload on the JWT Website.
Creating a Middleware to Verify Token Validity
Now we need to create a middleware — a function that runs between the request and response cycle, processing requests before they reach the final handler. This middleware ensures that only authenticated users can access protected routes.
import { FastifyRequest, FastifyReply } from "fastify";
import jwt, { JwtPayload } from "jsonwebtoken";
import { env } from "../env";
const secretKey = env.JWT_SECRET_KEY;
export const verifyToken = async (
request: FastifyRequest,
reply: FastifyReply
) => {
try {
const authorizationHeader = request.headers.authorization;
if (!authorizationHeader) {
return reply.status(401).send({ error: "Token not provided" });
}
const token = authorizationHeader.split(" ")[1];
const decoded = jwt.verify(token, secretKey);
if (typeof decoded === "string") {
return reply.status(401).send({ error: "Invalid token" });
}
request.user = decoded as JwtPayload & {
sub: string;
name: string;
username: string;
type: string;
iat: number;
exp: number;
};
} catch (err: any) {
console.error("Error on token validation:", err.message);
return reply.status(401).send({ error: "Invalid token" });
}
};
This function:
- Ensures a token is provided.
- Verifies the token's validity.
- Stores the decoded user data in
request.user
for later use.
Applying the Middleware and Testing Route Protection
To enforce authentication, add a preHandler
hook in server.ts
:
app.register(async (app) => {
app.addHook("preHandler", verifyToken);
// Protected routes go here
});
Now, test a protected route without a token. The response should be:
{
"error": "Token not provided"
}
To enable token authentication in Swagger UI, update fastifySwagger
configuration, adding the following code right after the info
object:
//...
info: {
title: "Blog API",
description: "API for my blog project.",
version: "1.0.0",
}, // ⬇️⬇️⬇️
securityDefinitions: {
BearerAuth: {
type: "apiKey",
name: "Authorization",
in: "header",
description: "Enter your JWT token in the format: Bearer ",
},
},
security: [{ BearerAuth: [] }],
// ...
A new "Authorize" button will appear. Click it and enter your token in the following format:
Bearer
Note: Don't forget to include "Bearer" before the token.
After authorization, protected routes will be accessible.
Conclusion
Now we have a simple but effective authentication system that allows public and protected routes in the API. If anything seems off, check out the project repository: Blog - by micaelmi.
Further improvements could include role-based access control (RBAC) to set different permissions for different users or another user-type validation process. But for now, we have a fully functional authentication system.