Warlock.js From Nodejs Course Into a fine grained framework
Tags: typescript, node, framework, mongodb
Published: 2024-06-20 14:35:44

## Introduction

It all started in the beginnings of 2023, where I started a new course here on dev Titled with: [Nodejs Course 2023](https://dev.to/hassanzohdy/nodejs-course-2023-introduction-to-nodejs-3n4e) that we start building the entire project from scratch.

Every single file there were written during the articles writing time, after achieving a very good progress, "that would be enough" I said to myself for the course.

But it didn't stop there, I started working on imporving the source code by using it in a real project, which was actually my own project [mentoor.io](https://mentoor.io) that's were things were taken to the next level.

After spending hundrueds of hours on developing the code, It has became more sable, rich with features and blazingly fast (thanks to Fastify) for handling http requests, at that point I decided to move on from being just a good project core to use to be a fully functional framework.

## Warlock.js Framework 🧙‍♂️

[Warlock.js](http://warlock.js.org/) is a nodejs framework that's built on top of Fastify, it's designed to be a very simple, fast and easy to use framework, It's main use is for building from small to large API applications.

### Features 🌟

- Blazing fast performance 🚀
- Hot Module Reload (MHR) for an incredibly fast development experience 🔄
- Module-Based Architecture 📂
- Entry Point (main.ts) for each module 🗂️
- Event-Driven Architecture 📅
- Auto Import for Events, Routes, Localization, and Configurations ⚙️
- Comprehensive request validation ✔️
- Mail Support with React Components 📧
- +6 Cache Drivers, including Redis 🗄️
- Grouped Routes by prefix or middleware 🛤️
- Internationalization Support (i18n) 🌍
- File Uploads with image compression and resizing 📸
- AWS Upload Support ☁️
- Postman Generator for API Documentation 📜
- Repositories using the Repository Design Pattern for managing database models 🗃️
- RESTful API support with a single base class for each module 🌐
- User Management and Auth using JWT 🔐
- Model Data Mapping when sending responses using Outputs 🗺️
- Full Support for MongoDB backed by Cascade
- VS Code Extension for generating modules, request handlers, response outputs, database models, RESTful classes, and repositories 🛠️
- Unit Testing Support for testing application API endpoints and modules 🧪
- Auto Generate incremental id for each model 🆔


And much more, you can check the full documentation [here](https://warlock.js.org/) 📚.

## Getting Started 🎉

To start a new project, just run the following command:

`npx create-warlock`

Then follow the instructions, and you will have a new project ready to go.

## Project Structure 🏗

Now let's have a quick look at the project structure:

```
├── src
│   ├── app
│   │   ├── home
│   │   │   ├── controllers
│   │   │   ├── routes.ts
│   │   ├── uploads
│   │   │   ├── routes.ts
│   │   ├── users
│   │   │   ├── controllers
│   │   │   │   ├── auth
│   │   │   │   │   ├── social
│   │   │   │   ├── profile
│   │   │   │   ├── restful-users.ts
│   │   │   ├── events
│   │   │   ├── mail
│   │   │   ├── models
│   │   │   │   ├── user
│   │   │   ├── output
│   │   │   ├── repositories
│   │   │   ├── utils
│   │   │   ├── validation
│   │   │   ├── routes.ts
│   │   ├── utils
│   │   ├── main.ts
│   ├── config
│   │   ├── app.ts
│   │   ├── auth.ts
│   │   ├── cache.ts
│   │   ├── cors.ts
│   │   ├── database.ts
│   │   ├── http.ts
│   │   ├── index.ts
│   │   ├── mail.ts
│   │   ├── upload.ts
│   │   ├── validation.ts
│   ├── main.ts
├── storage
├── .env
├── warlock.config.ts
```

### Auto Import Routes 🚗

So basically, the app is divided into modules, each module has its own folder, and inside it, there are controllers, routes, events, mail, models, output, repositories, utils, and validation.

The `routes.ts` file is a special file for each module, it is `auto` imported and called directly, so all you need is to define your routes there.

## Example

Let's see an example of a simple route:

```ts
// src/app/categories/routes.ts
import { router } from "@warlock.js/core";
import { getCategories } from "./controllers/get-categories";

router.get("/categories", getCategories);
```

Here we defined a simple route that listens to `GET /categories` and calls the `getCategories` controller.

Let's create our request handler:

```ts
// src/app/categories/controllers/get-categories.ts
import {
  type RequestHandler,
  type Request,
  type Response,
} from "@warlock.js/core";
import { categoriesRepository } from "./../repositories/categories-repository";

export const getCategories: RequestHandler = async (
  request: Request,
  response: Response
) => {
  const { documents: categories, paginationInfo } =
    await categoriesRepository.listActive(request.all());

  return response.send({
    categories,
    paginationInfo,
  });
};
```

So basically we called the repository to list our `active` categories where `isActive` is set to `true`

> You can change it in the repository settings to whatever field/value you use in database.

## Request Validation

Validation is as much as simple as it should be, you can define your validation schema in the tail of the request handler

```ts
// src/app/categories/controllers/add-category.ts

import {
  type RequestHandler,
  type Request,
  type Response,
  ValidationSchema,
} from "@warlock.js/core";

import { categoriesRepository } from "./../repositories/categories-repository";

export const addCategory: RequestHandler = async (
  request: Request,
  response: Response
) => {
  // using request.validated will return the validated data only
  const { name, description } = request.validated();

  const category = await categoriesRepository.create({
    name,
    description,
  });

  return response.send(category);
};

addCategory.validation = {
  rules: new ValidationSchema({
    name: ["required", "string", "minLength:6"],
    description: ["required", "string"],
  }),
};
```

If the validation fails on any rule, it won't reach the controller, and it will return a validation error response.

## A look into the database model

So database models are easy to work with, just define the model, cast the data as it should be there and that's it!

```ts
// src/app/categories/models/category.ts
import { type Casts, Model } from "@warlock.js/cascade";

export class Category extends Model {
  public static collection = "categories";

  protected casts: Casts = {
    name: "string",
    description: "string",
    isActive: "boolean",
  };
}
```

> You don't need to define `id` as the model will auto generate one for each new saved document.

## Data Syncing

One of the powered features of [Cascade](https://warlock.js.org/docs/cascade/getting-started/introduction/) Is [Syncing Models](https://warlock.js.org/docs/cascade/relationships/syncing-models), what does that mean?

Consider we have an `author` for a `post`, that author's data is changed at some point, in that case we need to update the post's author data as well, and that's what syncing models do.

```ts
// src/app/posts/models/post.ts
import { type Casts, Model } from "@warlock.js/cascade";
import { User } from "app/users/models/user";

export class Post extends Model {
  public static collection = "posts";

  protected casts: Casts = {
    title: "string",
    content: "string",
    author: User,
  };
}
```

Now let's define our `User` model, that's where the magic happens:

```ts
// src/app/users/models/user.ts
import { type Casts, Model } from "@warlock.js/cascade";
import { Post } from "app/posts/models/post";

export class User extends Model {
  public static collection = "users";

  /**
   * Sync the list of the given model when the user data is changed
   */
  public syncWith = [Post.sync("auth")];

  protected casts: Casts = {
    name: "string",
    email: "string",
  };
}
```

So basically, here we are telling the model, when the user's info is updated, find all posts for that author (user) and update the `author` field with the new data.

We can also, conditionally tell the model when to sync the data, for example, we can sync the data only when the user's name is changed:

```ts
// src/app/users/models/user.ts
import { type Casts, Model } from "@warlock.js/cascade";
import { Post } from "app/posts/models/post";

export class User extends Model {
  public static collection = "users";

  /**
   * Sync the list of the given model when the user data is changed
   */
  public syncWith = [Post.sync("auth").updateWhenChange(["name"])];

  protected casts: Casts = {
    name: "string",
    email: "string",
  };
}
```

Using `updateWhenChange` will only update the post's author when the user's name is changed.

## Models Relationships

Now let's go to the relationships, assume we are going to fetch the post's author when we fetch the post, that's where the relationships come in.

```ts
// src/app/posts/models/post.ts

import { type Casts, Model } from "@warlock.js/cascade";
import { User } from "app/users/models/user";

export class Post extends Model {
  public static collection = "posts";

  public static relations = {
    author: User.joinable("author.id").single(),
  };

  protected casts: Casts = {
    title: "string",
    content: "string",
  };
}
```

We told the model it is a single relation, and it's joinable by the `author.id` field.

Now when we fetch the post, the author's data will be fetched as well when calling `with` method.

```ts
const post = await Post.aggregate().where("id", 1).with("author").first();

const authorName = post.get("author.name");
```

We may also make the relation as a list, for example, if we have a `post` that has many `comments`:

```ts
// src/app/posts/models/post.ts
import { type Casts, Model } from "@warlock.js/cascade";
import { Comment } from "app/comments/models/comment";

export class Post extends Model {
  public static collection = "posts";

  public static relations = {
    comments: Comment.joinable("post.id"),
  };

  protected casts: Casts = {
    title: "string",
    content: "string",
  };
}
```

We can also filter comments by passing a query to the `with` method:

```ts
const post = await Post.aggregate()
  .where("id", 1)
  .with("comments", (query) => {
    query.where("isActive", true);
  })
  .first();
```

This could be useful to filter the data based on the relation, maybe to get comments only for current user,

```ts
import { useRequestStore } from "@warlock.js/core";

const post = await Post.aggregate()
  .where("id", 1)
  .with("comments", (query) => {
    const { user } = useRequestStore();
    query.where("createdBy", user.id);
  })
  .first();
```

Or even we can do it in the relation itself:

```ts
// src/app/posts/models/post.ts
import { type Casts, Model } from "@warlock.js/cascade";
import { Comment } from "app/comments/models/comment";

export class Post extends Model {
  public static collection = "posts";

  public static relations = {
    comments: Comment.joinable("post.id")
      .where("isActive", true)
      .select("id", "createdBy", "comment"),
  };

  protected casts: Casts = {
    title: "string",
    content: "string",
  };
}
```

The data will be return as an array in `comments` field, to change the return field name, you can use the `as` method:

```ts
// src/app/posts/models/post.ts
import { type Casts, Model } from "@warlock.js/cascade";
import { Comment } from "app/comments/models/comment";

export class Post extends Model {
  public static collection = "posts";

  public static relations = {
    comments: Comment.joinable("post.id")
      .where("isActive", true)
      .select("id", "createdBy", "comment")
      .as("postComments"),
  };

  protected casts: Casts = {
    title: "string",
    content: "string",
  };
}
```

To return the comment in a model, call `inModel()` method:

```ts
  public static relations = {
    comments: Comment.joinable("post.id").where("isActive", true).select('id', 'createdBy', 'comment').inModel(),
  };
```

## Conclusion 🎉

That's it for now, I hope you enjoyed the article, and I hope you will enjoy using the framework as well.

I'll post more articles about the framework, and how to use it in real-world applications.

Thank you for reading, and have a great day! 🌟