DEV Community

Cédric Pierre
Cédric Pierre

Posted on • Edited on

Consume RESTFul API like a Pro with Fluentity 🔥

Bring Active Record-Like API Consumption to the Frontend with Fluentity, a framework agnostic library written in Typescript.

As developers, we often face the same dilemma when working with APIs:

We want simplicity, type safety, and clean syntax — but most solutions force us to choose between low-level control (like Axios) or heavy abstractions (like TanStack Query).
Some options are really nice but totally glued to a framework, like PiniaORM which works only with... Pinia (Vuejs).

Direct comparison

Let’s say you want to fetch a Comment with id 2 from User with id 1 and update it.

// Fetch the comment
const res = await fetch('/users/1/comments/2')
const comment = await res.json()

// Update the comment
const updateRes = await fetch('/users/1/comments/2', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    ...comment,
    content: 'Updated text'
  })
})
const updated = await updateRes.json()
Enter fullscreen mode Exit fullscreen mode

With Fluentity

const comment = await User.id(1).comments.find(2);
await comment.update({content: 'Updated text'});
Enter fullscreen mode Exit fullscreen mode

A bit of context…

Throughout my career, I’ve worked on many different projects — each with its own way of handling API data fetching.

Yet none of them offered a solution that was easy to use, type-safe, reusable, chainable, and framework-agnostic.

As a fullstack developer, I naturally think in objects. That’s why Laravel’s Eloquent, a classic implementation of Active Record, feels so intuitive to me — and I know I’m not alone.

I truly believe Eloquent is a big part of Laravel’s popularity.

So I had an idea: what if we brought the elegance of Active Record to the frontend, for APIs?

That’s how Fluentity was born.

⚠️ Note: This project is still young and actively developed. Feedback is welcome!


What is Fluentity?

Fluentity is a lightweight, framework-agnostic TypeScript library that turns your endpoints into real models — with chainable methods, auto-casting, caching, and strong typing.

It also provides a lot of flexibility, by exposing request and response interceptors, overriding the request handler,...

How to configure Fluentity

Whether you’re working in Vue, Nuxt, React, or any other frontend stack, Fluentity gives you an elegant way to query your API without boilerplate.

Working together

BRO TIP: If your backend is already written in Typescript (NestJS), you can share the DTO between backend and frontend. There's even a way to reuse the same validators!

There's also a CLI tool to allow you to create models or to convert an OpenAPI schema file into Models !


Getting Started

📦 Installation

npm install @fluentity/core
# or
yarn add @fluentity/core
# or
pnpm install @fluentity/core
Enter fullscreen mode Exit fullscreen mode

Enable Typescript decorators:

{
  "compilerOptions": {
    "target": "ESNext",
    "experimentalDecorators": true,
    "useDefineForClassFields": false
  }
}
Enter fullscreen mode Exit fullscreen mode

Configuration

The most basic configuration requires to define the baseUrl.
You can refer to the documentation for more informations.

import { Fluentity, RestAdapter } from '@fluentity/core';

const fluentity = Fluentity.initialize({
    adapter: new RestAdapter({
        baseUrl: 'https://api.example.com'
    })
});
Enter fullscreen mode Exit fullscreen mode

Creating models

Fluentity is inspired by Active Record and Laravel Eloquent.

đź§± Example: User & Media Models

User.ts

import {
  Model,
  HasMany,
  Relation,
  Attributes,
  Cast,
  HasOne,
  RelationBuilder,
  Methods,
} from '../../src/index';

import { Company } from './Company';

import { Media } from './Media';
import { Thumbnail } from './Thumbnail';

import { QueryBuilder } from '../../src/QueryBuilder';

interface UserAttributes extends Attributes {
  name: string;
  phone: number;
  email: string;
  created_at?: string;
  updated_at?: string;
  thumbnail?: Thumbnail;
}

export class User extends Model<UserAttributes> implements UserAttributes {
  static resource = 'users';

  declare name: string;
  declare email: string;
  declare phone: number;

  declare created_at?: string;
  declare updated_at?: string;

  @HasMany(() => Media)
  medias!: Relation<Media[]>;

  @HasMany(() => Media, 'medias')
  libraries!: Relation<Media[]>;

  @HasMany(() => Media, 'custom-resource')
  customResource!: Relation<Media[]>;

  @HasOne(() => Media)
  picture!: Relation<Media>;

  @Cast(() => Thumbnail)
  thumbnail!: Thumbnail;

  @Cast(() => Thumbnail)
  thumbnails!: Thumbnail[];

  @Cast(() => Company)
  company!: Company;

  static scopes = {
    active: (query: RelationBuilder<User>) => query.where({ status: 'active' }),
  };

  static async login(username: string, password: string) {
    const queryBuilder = new QueryBuilder({
      resource: 'login',
      body: {
        username,
        password,
      },
      method: Methods.POST,
    });

    const response = await this.call(queryBuilder);

    return new this(response.data);
  }
}

Enter fullscreen mode Exit fullscreen mode

Media.ts

import { Model, Relation, BelongsTo } from '@fluentity/core'

import { Thumbnail } from './Thumbnail'

export interface MediaAttributes {
    id: string
    name: string
    size: string
    extension: string
    mime_type: string
    url: string
}

export class Media extends Model<MediaAttributes> {
    static resource = 'medias'
}
Enter fullscreen mode Exit fullscreen mode

Thumbnail.ts

import { Model } from '@fluentity/core'

export class Thumbnail extends Model<any> {
    static resource = 'thumbnails'
}
Enter fullscreen mode Exit fullscreen mode

Using Models

Now that the models are created, the hardest part is done! 🍻

const users = await User.all(); // This makes a GET /users and returns an array of User instances.

const user = await User.find(1); // Get /users/1 - return a user instance.

await user.update({name: 'Johana'}); // This send a PUT request to /users/:id
Enter fullscreen mode Exit fullscreen mode

But there is more! Let's try relationships or sub-resources:

Relationships

const medias = await User.id(1).medias.all(); // Get /users/1/medias - return an array of Media object

await media[0].delete(); // DELETE /users/1/medias/1
Enter fullscreen mode Exit fullscreen mode

Conditions:

const users = await User.where({name: 'Johana'}).all() // GET /users?name=johana
Enter fullscreen mode Exit fullscreen mode

Casting:

const user = await User.find(1);
console.log(user.thumbnail); // Thumbnail object
Enter fullscreen mode Exit fullscreen mode

Scopes:

const users = await User.query().active().all() // GET /users?status=active
Enter fullscreen mode Exit fullscreen mode

🚀 Conclusion

With Fluentity, consuming an API feels as natural as querying a database.
You define models once — and enjoy a fully typed, reusable, and object-oriented interface to your backend.

No more juggling between fetch(), Axios, and manually transforming responses.
Fluentity gives you a clean API, inspired by the elegance of Eloquent and Active Record, for the frontend world.

âś… Fluentity: Key Advantages :

  • Cleaner, declarative code with real models instead of imperative fetch/then chains.
  • Strong Typing: Safer development and fewer runtime bugs.
  • Framework-Agnostic: Use it anywhere TypeScript runs, including fullstack apps.
  • Chainable Query Builder: Express complex queries fluently.
  • Auto-Casting to Real Instances: Enables object-oriented logic across your app.
  • No Boilerplate: just define your models.
  • Customizable & Extensible: add custom scopes
  • Developer Experience (DX) Focus: Makes your API layer fun and productive to work with.

Bonus

I also created a CLI tool to generate models ❤️

fluentity generate:model [name]
Enter fullscreen mode Exit fullscreen mode

This command will parse an OpenAPI Schema and generates all the models for you:

fluentity parse:openapi [filename]
Enter fullscreen mode Exit fullscreen mode

Checkout this demo with Vue


👉 Star Fluentity on GitHub
đź§Ş Try it in your next project, and share your feedback!

Cheers,
Cédric

Linkedin
Github

Top comments (0)