Skip to content

MiaadTeam/lesan

Repository files navigation

بسم الله الرحمن الرحیم

Why Lesan?

Lesan is a blazing-fast, cross-platform web framework and Object Document Mapper (ODM) designed to give you the exact data-fetching flexibility of GraphQL, without its heavy processing overhead, while maintaining the simplicity of REST.

Even though NoSQL is very fast, its complexities are very troublesome for large-scale projects. On the other hand, GraphQL shines in client-server connectivity but it has several weaknesses and is somewhat complex, adding another layer of complexity to the project. That’s why we created LESAN.

🚀 Now Cross-Platform! Lesan natively supports Node.js, Bun, and Deno with zero configuration. Write your code once and run it anywhere!

✨ What makes Lesan special?

  • 🎯 Client-Driven Projections: Similar to GraphQL, clients dictate exactly the shape and depth of the data they receive, including nested relations, but without the CPU cost of parsing a complex query language.
  • 🤝 One-Directional Magic, Bi-Directional Power: Relationships in Lesan are an absolute game-changer. You define a relationship from one direction (the model that needs it most), and Lesan automatically handles the immensely complex bi-directional embedding, syncing, array limits, sorting, and cascading updates/deletes behind the scenes. Goodbye to complex JOINs and manual NoSQL array updates!
  • 🛡️ End-to-End Type Safety: Run your server with the typeGeneration: true flag, and Lesan will auto-generate perfect client-side TypeScript definitions and a custom fetch wrapper. This guarantees flawless frontend integration and autocomplete.
  • Insane Performance: Stripped of unnecessary middleware bloat and built directly on the official MongoDB driver, Lesan's direct-to-database routing crushes traditional REST/ORM and GraphQL server speeds.
  • 🧩 Built for Microservices: The action-based architecture inherently supports breaking large monolithic systems down into isolated microservices effortlessly.

This video is an introductory tutorial on the Lesan framework in the Farsi language.

Benchmarks

benchmark-animation

We use this formula to calculate the difference : (B - A) ÷ A * 100
As you see on the chart:

  • Lesan return data to client 1168% faster than the prisma-express-rest. Which uses postgres as a database.
  • Lesan return data to client 1417% faster than the prisma-express-graphql. Which uses postgres as a database.
  • Lesan return data to client 4435% faster than the mongoose-express-rest (Note that we did not sort in this query)
  • Lesan return data to client 72289% faster than the mongo-express-rest (Note that we did not sort in this query)
  • Lesan return data to client 298971% faster than the mongoose-express-rest (used sortby)

Maybe we created the most performant framework in the world! see more detailed benchmark

Note: With our new cross-platform architecture, Lesan achieves over 10,000 requests per second on Bun and Deno! Check out our Cross-Platform Benchmarks.

Documentation

Installation

Lesan is designed to work seamlessly across all major JavaScript runtimes.

Node.js

npm install @hemedani/lesan mongodb

Bun

bun add @hemedani/lesan mongodb

Deno

import { lesan } from "jsr:@hemedani/lesan";
import { MongoClient } from "npm:mongodb";

A little trip

Look at the code below:

Create a file called main.ts (or main.js) and paste the code below into it:

import {
  ActFn,
  Document,
  Filter,
  lesan,
  number,
  object,
  ObjectId,
  optional,
  size,
  string,
} from "@hemedani/lesan"; // Use "jsr:@hemedani/lesan" for Deno
import { MongoClient } from "mongodb"; // Use "npm:mongodb" for Deno

const coreApp = lesan();

const client = await new MongoClient("mongodb://127.0.0.1:27017/").connect();
const db = client.db("civil");

coreApp.odm.setDb(db);

// ================== MODEL SECTION ==================
// ------------------ Country Model ------------------
const countryPure = {
  name: string(),
  population: number(),
  abb: string(),
};
const countryRelations = {};
const countries = coreApp.odm.newModel("country", countryPure, countryRelations);

// ------------------ User Model ------------------
const userPure = {
  name: string(),
  age: number(),
};
const users = coreApp.odm.newModel("user", userPure, {
  country: {
    optional: false,
    schemaName: "country",
    type: "single",
    relatedRelations: {
      users: {
        type: "multiple",
        limit: 50,
        sort: {
          field: "_id",
          order: "desc",
        },
      },
    },
  },
});

// ================== FUNCTIONS SECTION ==================
// ------------------ Add Country ------------------
const addCountryValidator = () => {
  return object({
    set: object(countryPure),
    get: coreApp.schemas.selectStruct("country", { users: 1 }),
  });
};

const addCountry: ActFn = async (body) => {
  const { name, population, abb } = body.details.set;
  return await countries.insertOne({
    doc: {
      name,
      population,
      abb,
    },
    projection: body.details.get,
  });
};

coreApp.acts.setAct({
  schema: "country",
  actName: "addCountry",
  validator: addCountryValidator(),
  fn: addCountry,
});

// ------------------ Get Countries  ------------------
const getCountriesValidator = () => {
  return object({
    set: object({
      page: number(),
      limit: number(),
    }),
    get: coreApp.schemas.selectStruct("country", 1),
  });
};

const getCountries: ActFn = async (body) => {
  let {
    set: { page, limit },
    get,
  } = body.details;

  page = page || 1;
  limit = limit || 50;
  const skip = limit * (page - 1);

  return await countries.find({ projection: get, filters: {} }).skip(skip).limit(limit).toArray();
};

coreApp.acts.setAct({
  schema: "country",
  actName: "getCountries",
  validator: getCountriesValidator(),
  fn: getCountries,
});

// --------------------- Add User ----------------------
const addUserValidator = () => {
  return object({
    set: object({
      ...userPure,
      country: string(),
    }),
    get: coreApp.schemas.selectStruct("user", 1),
  });
};
const addUser: ActFn = async (body) => {
  const { country, name, age } = body.details.set;

  return await users.insertOne({
    doc: { name, age },
    projection: body.details.get,
    relations: {
      country: {
        _ids: new ObjectId(country),
        relatedRelations: {
          users: true,
        },
      },
    },
  });
};

coreApp.acts.setAct({
  schema: "user",
  actName: "addUser",
  validator: addUserValidator(),
  fn: addUser,
});

// --------------------- Get Users ----------------------
const getUsersValidator = () => {
  return object({
    set: object({
      page: number(),
      take: number(),
      countryId: optional(size(string(), 24)),
    }),
    get: coreApp.schemas.selectStruct("user", { country: 1 }),
  });
};
const getUsers: ActFn = async (body) => {
  let {
    set: { page, limit, countryId },
    get,
  } = body.details;

  page = page || 1;
  limit = limit || 50;
  const skip = limit * (page - 1);
  const filters: Filter<Document> = {};
  countryId && (filters["country._id"] = new ObjectId(countryId));

  return await users.find({ projection: get, filters }).skip(skip).limit(limit).toArray();
};

coreApp.acts.setAct({
  schema: "user",
  actName: "getUsers",
  validator: getUsersValidator(),
  fn: getUsers,
});

// ================== RUN SECTION ==================
coreApp.runServer({ port: 1366, typeGeneration: false, playground: true });

Run the server

Depending on your runtime, run the following command in your terminal:

Node.js (requires tsx or similar for TypeScript):

npx tsx main.ts

Bun:

bun run main.ts

Deno:

deno run -A main.ts

You should see this message:

HTTP webserver running.
please send a post request to http://localhost:1366/lesan
you can visit playground on http://localhost:1366/playground

Listening on http://localhost:1366/

Now you can visit the playground at http://localhost:1366/playground and send requests to the server for addCountry, addUser, and getUsers. Screen Shot 1402-04-26 at 20 47 05

Alternatively, you can send a POST request to http://localhost:1366/lesan with postman including the following in JSON format inside the body in order to retrieve the desired data:

{
  "service": "main",
  "model": "country",
  "act": "addCountry",
  "details": {
    "set": {
      "name": "Iran",
      "population": 85000000,
      "abb": "IR"
    },
    "get": {
      "_id": 1,
      "name": 1,
      "population": 1,
      "abb": 1
    }
  }
}

Screen Shot 1402-04-25 at 18 24 16

The Magic of Lesan Relationships ✨

In Lesan, relationships are One-Directional in definition, but fully embedded bi-directionally. When you link a User to a Country, Lesan seamlessly embeds the country details inside the user, and automatically embeds the user details inside the country's users array (even keeping the top 50 users sorted automatically!).

We handle all the heavy lifting of keeping this embedded data perfectly in sync across your entire database for blazing-fast reads. You can also control the exact level of penetration into the relationship depth. On the client-side, you simply describe what you want and get back exactly what you described—with zero backend refactoring!

Contributors

Many thanks to those who supported us

Mehrshad Cheshm Khavari
Mehrshad Cheshm Khavari
Mahdi Ramezani
Mahdi Ramezani
Alireza Safaierad
Alireza Safaierad
ali davodi
ali davodi
sorena ganji
sorena ganji
Node Master
Node Master
Alireza Safaierad
Alireza Safaierad
Erfan Asadi
Erfan Asadi