Skip to main content
imvinojanv.dev
  • About
  • Blog
  • Projects
  • Snippets

Command Palette

Search for a command to run...

imvinojanv.dev
Open to Hire

I'm always open to discussing software engineering work or partnership.

HomeAboutResumeUses
BlogProjectsSnippets
© 2026 Vinojan Veerapathirathasan —— Colombo, Sri Lanka.
MediumGitHubLinkedInRSS
Back

Build a Token based RESTful APIs with Fastify, Prisma & TypeScript — Part 1

Step-by-step guide to create token-based RESTful APIs with Fastify, Prisma, JWT, and TypeScript for secure authentication.

Published on April 30, 2024
16 min read
Build a Token based RESTful APIs with Fastify, Prisma & TypeScript — Part 1

Hi there 👋, Are you wondering to building an scalable web application? You’ll probably need user authentication. It is our responsibility as developers to protect user data and make sure that only people with permission can access resources that are protected.

Building scalable and maintainable APIs has become a fundamental skill. RESTful APIs serve as the backbone of modern web applications, enabling seamless communication between client-side applications and back-end services. If you’re new to API development or looking to expand your toolkit, this guide is for you 🫵

In this step-by-step article, I’ll guide you through building a robust token based RESTful API from scratch using a powerful tech stack: Fastify, JWT, TypeScript, Node.js, and Prisma. Fastify, known for its speed and low overhead, is an ideal choice for creating high-performance APIs. TypeScript enhances code quality with static typing, while Prisma simplifies database interactions. PostgreSQL, a leading open-source relational database, will serve as the backbone for data storage and retrieval.

To ensure your API is well-documented and easy to test, On the next part, I’ll also cover integrating Swagger for automated API documentation and using Postman for testing API endpoints. By the end of this article, you’ll not only understand the fundamental concepts behind token based authentication and RESTful APIs but also have a practical implementation that you can integrate into your projects.

However, I divided this article into two parts Part 1: Set up the project & Implementation of token based authentication. Part 2: Implementing the product routes & Documentation. (Swagger)

Whether you’re a seasoned developer or just starting, this article will equip you with the knowledge and tools to create APIs that can scale with your projects and stand the test of time. Let’s dive in and start building!

Required basic skills

  • Node.js & TypeScript

Before you start the development, Set up the Postman collections and environment variables (I wrote a separate guide for you☝️*, please read it and set up your Postman)*

#1: Initialize the project

  1. Create a new folder for this project and open it
mkdir fastify-api-tutorial
cd fastify-api-tutorial
  1. Initiate npm
npm init -y
  1. Install TypeScript globally. You can use this command to install TypeScript globally, this means that you can use the tsc command anywhere in your terminal
npm install -g typescript
// Init TypeScript
npx tsc --init
  1. Install other dependencies
// Dependencies
npm install @prisma/client fastify fastify-zod zod zod-to-json-schema @fastify/jwt
// Dev dependencies
npm install ts-node-dev typescript @types/node --save-dev
  1. Create the src folder, and create app.ts into the folder

  2. Write the basic code to run server and test the helloworld endpoint

src/app.ts
import Fastify from "fastify";
const server = Fastify();
server.get("/helloworld", async (req, res) => {
  return { message: "Hello World!" };
});
async function main() {
  try {
    await server.listen({ port: 3000, host: "0.0.0.0" });
    console.log("Server listening at http://localhost:3000");
  } catch (error) {
    console.error(error);
    process.exit(1); // exit as failure
  }
}
main();
  1. Add the run script into package.json file
package.json
"scripts": {
    "dev": "tsnd --respawn --transpile-only --exit-child src/app.ts"
},
  1. Run this command npm run dev to run the application.
// Test the API endpoint on Postman
GET http://localhost:3000/helloword
// Response
{
  message: 'Hello World!'
}

#2: Structure the project

  1. Structure the application
File Structure
.
├── src
│  ├── app.ts
│  ├── modules
│  │  ├── product
│  │  └── user
│  │    ├── user.route.ts
│  │    ├── user.schema.ts
│  │    ├── user.controller.ts
│  │    └── user.service.ts
│  └── utils
│     ├── hash.ts
│     └── prisma.ts
├── prisma
│  └── schema.prisma
├── package.json
├── tsconfig.json
├── global.d.ts
└── .env
  1. Create files for controller, schema, route, and service inside the folder. Ex: In our user module, we have 4 files
  • user.route.ts ⇒ handling user routes
  • user.schema.ts ⇒ handling input and response schemas
  • user.controller.ts ⇒ main logic of each route
  • user.service.ts ⇒ handling the functions for db connections
  1. Implement the user route
src/modules/user/user.route.ts
import { FastifyInstance } from "fastify";
import { registerUserHandler } from "./user.controller";
async function userRoutes(server: FastifyInstance) {
  server.post("/", registerUserHandler);
}
export default userRoutes;
  1. Import the userRoute into app.ts and register the route
src/app.ts
import Fastify from "fastify";
import userRoutes from "./modules/user/user.route";
const fastify = Fastify();
async function main() {
  fastify.register(userRoutes, { prefix: "api/users" });
  // other codes
}
main();
  1. Implement the registerUserHandler controller and import it into the userRoutes
src/modules/user/user.controller.ts
import { FastifyReply, FastifyRequest } from "fastify";
export async function registerUserHandler(
  request: FastifyRequest,
  reply: FastifyReply,
) {
  // TODO: Function code
}

#3: Setting up Prisma ORM

  1. Initiate the prisma NOTE: When you run this code, Prisma folder and .env file will be automatically created within your project
npx prisma init --datasource-provider postgresql
  1. Create a new database on neon.tech and copy the DATABASE_URL and paste it into the .env file

  2. Create the prisma schema models

prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
model User {
  id       Int       @id @default(autoincrement())
  email    String    @unique
  name     String?
  password String
  salt     String
  products Product[]
}
model Product {
  id        Int      @id @default(autoincrement())
  title     String   @db.VarChar(255)
  content   String?
  price     Float
  ownerId   Int
  owner     User     @relation(fields: [ownerId], references: [id], onDelete: Cascade)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  @@index([ownerId])
}
  1. Migrate the schema models. (will be created the migrations folder inside the prisma
npx prisma migrate dev --name init
  1. Create the utils/prisma.ts file to make the prisma connection
src/utils/prisma.ts
import { PrismaClient } from "@prisma/client";
declare global {
  var prisma: PrismaClient | undefined;
}
export const db = globalThis.prisma || new PrismaClient();
if (process.env.NODE_ENV !== "production") globalThis.prisma = db;

Here, I am using a proper method which can be used in production environment as well. It will avoid some hydration errors.🤗

  1. Implement the user schema with zod
src/modules/user/user.schema.ts
import * as z from "zod";
const createUserSchema = z.object({
  email: z
    .string({
      required_error: "Email is required",
      invalid_type_error: "Email is not valid",
    })
    .email(),
  name: z.string(),
  password: z.string({
    required_error: "Password is required",
  }),
});
export type CreateUserInput = z.infer<typeof createUserSchema>;
  1. Implement the user service for createUser function and import the CreateUserInput type
src/modules/user/user.service.ts
import { db } from "../../utils/prisma";
import { CreateUserInput } from "./user.schema";
export async function createUser(input: CreateUserInput) {
  const user = await db.user.create({
    data: input, // here error might come, it will be fixed.
  });
}

#4: Hash the Password

  1. Create hash.ts file into utils
  2. Create 2 functions called hashPassword, verifyPassword into the hash.ts file
src/utils/hash.ts
import crypto from "crypto";
export function hashPassword(password: string) {
  const salt = crypto.randomBytes(16).toString("hex");
  const hash = crypto
    .pbkdf2Sync(password, salt, 1000, 64, "sha512") // (password: crypto.BinaryLike, salt: crypto.BinaryLike, iterations: number, keylen: number, digest: string)
    .toString("hex");
  return { hash, salt };
}
export function verifyPassword({
  candidatePassword,
  salt,
  hash,
}: {
  candidatePassword: string;
  salt: string;
  hash: string;
}) {
  const candidateHash = crypto
    .pbkdf2Sync(candidatePassword, salt, 1000, 64, "sha512")
    .toString("hex");
  return candidateHash === hash;
}

Let’s take a look at what is happening above: hashPassword : Generate the salt, and using it hash the password. verifyPassword : Taking the inputted password, salt, and hash then compare them to check the validity of the password.

  1. Update the createUser function
src/modules/user/user.service.ts
export async function createUser(input: CreateUserInput) {
  const { password, ...rest } = input;
  const { hash, salt } = hashPassword(password);
  const user = await db.user.create({
    data: { ...rest, salt, password: hash },
  });
  return user;
}

Let’s take a look at what is happening above:

  • First we separated the password from input
  • We call the hashPassword function and pass the password
  • Then, create the user data with the hashed password and salt
  • Finally, return created user
  1. Update the registerUserHandler function
src/modules/user/user.controller.ts
import { FastifyReply, FastifyRequest } from "fastify";
import { CreateUserInput } from "./user.schema";
import { createUser } from "./user.service";
export async function registerUserHandler(
  request: FastifyRequest<{
    Body: CreateUserInput;
  }>,
  reply: FastifyReply,
) {
  const body = request.body;
  try {
    const user = await createUser(body);
    return reply.status(201).send(user);
  } catch (error) {
    console.error(error);
    reply.status(500).send({
      message: "Internal Server Error",
      error: error,
    });
  }
}

To properly sending back the response to the front-end,

  1. we need to re-define the user scheme like this:
src/modules/user/user.schema.ts
import * as z from "zod";
import { buildJsonSchemas } from "fastify-zod";
const userCore = {
  // define the common user schema
  email: z
    .string({
      required_error: "Email is required",
      invalid_type_error: "Email is not valid",
    })
    .email(),
  name: z.string(),
};
const createUserSchema = z.object({
  ...userCore, // re-use the userCore object
  password: z.string({
    required_error: "Password is required",
  }),
});
const createUserResponseSchema = z.object({
  id: z.number(),
  ...userCore,
});
export type CreateUserInput = z.infer<typeof createUserSchema>;
export const { schemas: userSchemas, $ref } = buildJsonSchemas({
  createUserSchema,
  createUserResponseSchema,
});

In here, We need to attach this createUserResponseSchema to our route (_‘/’_) to say only respond with these properties. And the way that we’re going to do that is with a fastify plugin called fastify-zod

  1. Inside our main function we wanna register this schema:
src/app.ts
// ...
import { userSchemas } from "./modules/user/user.schema";
async function main() {
  for (const schema of userSchemas) {
    // should be add these schemas before you register your routes
    server.addSchema(schema);
  }
  server.register(userRoutes, { prefix: "api/users" }); // routes register
  // try-catch block
}
main();
  1. We are going to define the object options :
src/modules/user/user.route.ts
// ...
import { $ref } from "./user.schema";
async function userRoutes(server: FastifyInstance) {
  server.post(
    "/",
    {
      schema: {
        body: $ref("createUserSchema"),
        response: {
          201: $ref("createUserResponseSchema"),
        },
      },
    },
    registerUserHandler,
  );
}

Now we can try with the create user endpoint. It’s work 👍

Figure 1: Register user response

#5: Setting up JWT and cookies

  1. Register the JWT and cookie inside the app.ts
src/app.ts
import Fastify, { FastifyRequest, FastifyReply } from "fastify";
import fjwt, { FastifyJWT } from "@fastify/jwt";
import fCookie from "@fastify/cookie";
const fastify = Fastify();
fastify.register(fjwt, {
  secret: process.env.JWT_SECRET || "some-secret-key",
});
fastify.addHook("preHandler", (req, res, next) => {
  req.jwt = fastify.jwt;
  return next();
});
fastify.register(fCookie, {
  secret: process.env.COOKIE_SECRET || "some-secret-key",
  hook: "preHandler",
});
// other codes

Let’s take a look at what is happening above:

  • First we imported both
  • Register the @fastify/jwt and pass the secret from .env - Then we created a hook and passed the app.jwt to its request object. In Fastify, a prehandler hook is a powerful and flexible feature that allows you to execute logic before a route handler is called. It provides a way to perform tasks such as authentication, validation, data transformation, or any other processing that should occur prior to the actual route handler being invoked.
  • Finally, we register our @fastify/cookie . The hook option allows you to determine at which stage of request processing the plugin should handle cookies.

I am sure your typescript is screaming at you 🤬, What is req.jwt

So to fix this, we need to let fastify know, what this is. Create a global.d.ts file on root. and include it on tsconfig.json

global.d.ts
// global.d.ts
import { JWT } from "@fastify/jwt";
declare module "fastify" {
  interface FastifyRequest {
    jwt: JWT;
  }
}
tsconfig.json
{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "types": ["node"]
  },
  "include": ["src/**/*.ts", "global.d.ts"],
  "exclude": ["node_modules"]
}

Your typescript will be happy now. Note: We will add more lines to this file soon. Because Typescript is a strict parent 😒

Generate the SECRET for JWT and put it into your .env file as JWT_SECRET ( OnlineKeyGenerator: https://it-tools.tech/token-generator?length=64 )

  1. Create the login route
src/modules/user/user.route.ts
// user.route.ts
async function userRoutes(server: FastifyInstance) {
  // registerUserHandler
  server.post("/login", {}, loginHandler);
}
  1. Create the login schema and login response schema
src/modules/user/user.schema.ts
// user.schema.ts
// other codes
const loginSchema = z.object({
  email: z
    .string({
      required_error: "Email is required",
      invalid_type_error: "Email is not valid",
    })
    .email(),
  password: z.string(),
});
const loginResponseSchema = z.object({
  accessToken: z.string(),
});
// ...
export type LoginInput = z.infer<typeof loginSchema>;
export const { schemas: userSchemas, $ref } = buildJsonSchemas({
  createUserSchema,
  createUserResponseSchema,
  loginSchema,
  loginResponseSchema,
});
  1. Create loginHandler controller function.
src/modules/user/user.controller.ts
// user.controller.ts
import { CreateUserInput, LoginInput } from "./user.schema";
import { createUser, findUserByEmail } from "./user.service";
import { verifyPassword } from "../../utils/hash";
// other codes
export async function loginHandler(
  request: FastifyRequest<{
    Body: LoginInput;
  }>,
  reply: FastifyReply,
) {
  const body = request.body;
  // Find a user by email
  const user = await findUserByEmail(body.email);
  if (!user) {
    return reply.status(401).send({
      message: "Invalid email address. Try again!",
    });
  }
  // Verify password
  const isValidPassword = verifyPassword({
    candidatePassword: body.password,
    salt: user.salt,
    hash: user.password,
  });
  if (!isValidPassword) {
    return reply.status(401).send({
      message: "Password is incorrect",
    });
  }
  // Generate access token
  const payload = {
    id: user.id,
    email: user.email,
    name: user.name,
  };
  const token = request.jwt.sign(payload);
  reply.setCookie("access_token", token, {
    path: "/",
    maxAge: 1000 * 60 * 60 * 24 * 7, // for a week
    httpOnly: true,
    secure: true,
  });
  return { accessToken: token };
}

Let’s take a look at what is happening above:

  • First, we get the body from the user, (validate it to prevent the SQL
  • Find user by email and check if this user exist or not.
  • If the user exist, call the verifyPassword function, and compare the passwords
  • If the passwords are matched, Create the JWT token
  • Then we securely set the cookie, so that the client always requests with this cookie in the header
  • Finally, we send back the token (also it can be manually used as a bearer token for authorization)
  1. Define the login route object with schema
src/modules/user/user.route.ts
// user.route.ts
import { loginHandler, registerUserHandler } from "./user.controller";
async function userRoutes(fastify: FastifyInstance) {
  // other codes
  fastify.post(
    "/login",
    {
      schema: {
        body: $ref("loginSchema"),
        response: {
          201: $ref("loginResponseSchema"),
        },
      },
    },
    loginHandler,
  );
}

It’s time to check, if it works or not. We use the email and password that we created earlier to login. Finally, It returns the token and also sets it to cookies. 👏

Figure 2: Login user response
Figure 3: Created the cookie

#6: Protect the Routes

We did the authentication part, now we can successfully register and login as a user. Now we are going to look at the most important application of what we have just done. Let’s come with me 🚶‍♂️

Note: We don’t need to protect all routes. There could be resources that can be accessible to all. So we will manually protect some routes that only authorized users can access.

For that, we can manually check if the request header has cookies and verify its token every time. But here is an alternative way, we can user fastify decorate for this.

decorate is a method that allows you to extend the functionality of Fastify’s code objects, such as: Fastify Instance (fastify), the request parameter (request), and the reply onject (reply). It’s a powerful feature that enables you to add custom properties, methods, or utilities to these objects, making them available throughout your Fastify application.

src/app.ts
// app.ts
// imports
const fastify = Fastify();
fastify.register(fjwt, {
  secret: process.env.JWT_SECRET || "imvinojan02061999xxxx",
});
fastify.decorate(
  "authenticate",
  async (request: FastifyRequest, reply: FastifyReply) => {
    const token = request.cookies.access_token;
    if (!token) {
      return reply.status(401).send({ message: "Authentication required" });
    }
    const decoded = request.jwt.verify(token);
    request.user = decoded;
  },
);
// other codes

Let’s take a look at what is happening above:

  • First, we are accessing the token
  • If there is no token, pass the response as unauthorized
  • If it’s there, then verify the token
  • Finally, we attach our current user payload to the request object.

Your typescript is screaming at you again 🤬, What is 'user' replace your global.d.ts file with this code to make your typescript happy☺

global.d.ts
// global.d.ts
import { JWT } from "@fastify/jwt";
declare module "fastify" {
  interface FastifyRequest {
    jwt: JWT;
  }
  export interface FastifyInstance {
    authenticate: any;
  }
}
type UserPayload = {
  id: number;
  email: string;
  name: string;
};
declare module "@fastify/jwt" {
  interface FastifyJWT {
    user: UserPayload;
  }
}
  1. Create get users route and protect it with authenticate preHandler.
src/modules/user/user.route.ts
// user.route.ts
import {
  getUsersHandler,
  loginHandler,
  registerUserHandler,
} from "./user.controller";
async function userRoutes(fastify: FastifyInstance) {
  // other codes
  fastify.get(
    "/",
    {
      preHandler: [fastify.authenticate],
    },
    getUsersHandler,
  );
}
  1. Create getUsersHandler controller and getUsers service function.
src/modules/user/user.controller.ts
// user.controller.ts
// other handler functions
export async function getUsersHandler() {
  const users = await getUsers();
  return users;
}
src/modules/user/user.service.ts
// user.service.ts
// other functions
export async function getUsers() {
  return db.user.findMany({
    select: {
      id: true,
      name: true,
      email: true,
    },
  });
}

It’s time to check whether we were successful in protecting our route or not. You can try with your Postman to get users with login and without login. (you can also check the response when you change or remove the authorizations token).

#7: Logout User

It’s very easy method, we just need to clear the cookies. That’s it 🤷

  1. Create logout route and use the preHandler
src/modules/user/user.route.ts
// user.route.ts
import {
  logoutHandler,
  getUsersHandler,
  loginHandler,
  registerUserHandler,
} from "./user.controller";
async function userRoutes(fastify: FastifyInstance) {
  // other codes
  fastify.delete(
    "/logout",
    {
      preHandler: [fastify.authenticate],
    },
    logoutHandler,
  );
}
  1. Create logoutHandler controller function
src/modules/user/user.controller.ts
// user.controller.ts
// other handler functions
export async function logoutHandler(
  request: FastifyRequest,
  reply: FastifyReply,
) {
  reply.clearCookie("access_token");
  return reply.status(201).send({ message: "Logout successfully" });
}

Let’s try this as well. It should clear your cookie.


I appreciate you taking the time to read this article.🙌

In this part, We have learned how to create a maintainable fastify back-end project and create the secure token based authentication APIs using JWT. I hope you also have a good experience with Postman through this part.

In the second part, we’ll cover implementing the product routes APIs and documenting them using Swagger. See you there! 🙌

Part 2: https://imvinojanv.dev/blog/build-a-token-based-restful-apis-with-fastify-prisma-typescript-part-2

Source code: https://github.com/imvinojanv/fastify-prisma-rest-api

Before you move on to explore the next article, don’t forget to give your claps 👏 for this article and share with your friends. Stay connected with me on social media. Thanks for your support and have a great rest of your day! 🎊

✍️ Vinojan Veerapathirathasan.

0
0
0
0
0

On This Page

Required basic skills#1: Initialize the project#2: Structure the project#3: Setting up Prisma ORM#4: Hash the PasswordTo properly sending back the response to the front-end,#5: Setting up JWT and cookies#6: Protect the Routes#7: Logout User
Last updated: April 30, 2024