Published on

Introducing Intent

Authors

The Paradox of Choice in NodeJS

The NodeJS ecosystem is rich with frameworks like NestJS, SailsJS, and Adonis. These frameworks offer extensive ecosystems of plugins and extensions, allowing developers to integrate databases, caches, and various services with ease. While this flexibility is powerful, it introduces a significant challenge: decision fatigue.

Simplicity is the ultimate sophistication

This timeless quote resonates strongly in today's software engineering landscape. The current NodeJS ecosystem, fragmented with countless packages, often complicates development rather than simplifying it. Every developer has their preferences, leading to projects with varying architectures and dependencies.

The Need for Consistency

During NodeJS, I found myself working on Node projects with vastly different architectures. Experimenting with various packages made it challenging for my team to understand and build upon our codebase. This inconsistency hampered our ability to:

  • Switch contexts quickly
  • Debug efficiently
  • Implement features rapidly

Learning from Other Ecosystems

Frameworks like Laravel and Ruby on Rails have kept PHP and Ruby relevant by offering superior features and an excellent Developer Experience (DX). Inspired by this, and particularly affirmed by a tweet from Taylor Otwell, I began to envision a similar solution for the NodeJS ecosystem.

The Birth of Intent

I will be honest, first version of Intent is an aggregation of my past open source works that I have been doing since 2020. It's built on top of NestJS utilising it's powerful Dependency Injection system and HTTP Server Implementation. Intent prioritizes Developer Experience (DX), and various integrations like FileSystems, Queues, Cache, etc.

Introduction

Intent is a web application framework built on top of NestJS. With built-in declarative and elegant APIs out-of-the-box, you can quickly build an production ready scalable application.

Some of the features that Intent supports out of the box.

IntegrationDrivers
RDBMSMySQL, Postgres, Sqlite
StorageUnix File Systems, S3
Message QueuesAWS SQS, Redis
MailersSMTP, Mailgun, Resend
CachingRedis, In-Memory
Console Commands
LoggingFile Based Logging
Exception HandlerSentry Integration
Validations
Transformers
HelpersNumber, Array, Objects and Strings
Internationalisation

Let's take a quick look at how quickly you can use these integrations.

Database Integration

DB Introduction here.

Using Storage

Intent supports UNIX File System and AWS S3 right now. All you need to change is the configuration, and done. No change at the code level is needed.

// reads a file
await Storage.disk("invoices").get("order_1234.pdf");

// uploads a file
await Storage.disk("invoices").put("order_23456.pdf", content, { 
  mimeType: "application/pdf" 
});

Read More - Storage Docs

Message Queues

For any async tasks, Intent provides support for Redis-based, AWS SQS message queues. Let's take a quick look.

First, define a job with Job decorator.

import { Injectable } from '@nestjs/common';
import { Job } from '@intentjs/core';

@Injectable()
export class NotificationJob {
  constructor() {}

  @Job('user_signedup')
  async create(data: Record<string, any>) {
    // write your logic here
  }
}

Now, we need to dispatch this job to the queue.

Dispatch({
  job: "user_signedup",
  data: { email: "vinayak@tryintent.com", subject: "Thanks for signing up.", },
});

Now you can start consuming the messages using node intent queue:work command in terminal.

Read More - Queue Docs

Mailers

Intent comes with an in-built email template which you can use to build emails. For example,

import { MailMessage } from '@intentjs/core';

const mail = MailMessage.init()
  .greeting('Hey there')
  .line(
    'We received your request to reset your account password.',
  )
  .button('Click here to reset your password', 'https://google.com')
  .line('Alternative, you can also enter the code below when prompted')
  .inlineCode('ABCD1234')
  .line('Rise & Shine,')
  .line('V')
  .subject('Hey there from Intent')

Above code will output the following email,

Mail Sample

Now you can simply send the email with any of the support drivers (Resend, Mailgun, SMTP).

import { Mail } from "@intentjs/core";

Mail.init()
  .to("vinayak@tryintent.com") // OR .to(['id1@email.com', 'id2@email.com'])
  .send(mail);

Read More - Mail Docs

Caching

Intent comes out of the support for Redis and In Memory cache database. Let's see how quickly you can start using cache.

// setting value in cache
await Cache.store().set("otp", 1234);

// getting value from cache
await CacheStore().get("otp");

You might also run into situations where the data doesn't exist in the cache, then you can use the remember or rememberForever method.

const cb = () => {
  // your custom logic here, for eg. a db query, an api call.
  return [
    { name: 'Shoe Dog', author: 'Phil Knight', },
  ];
};

await CacheStore().remember("books", cb, 120);

Read More - Cache Docs

Console Commands

You can also write elegant console commands using Intent which you can run in your terminal using node intent command_name.

import { Injectable } from "@nestjs/common";
import { Command, ConsoleIO } from "@intentjs/core";

@Injectable()
@Command("hello {name=world}", { desc: "Test Command" })
export class HelloWorldCommand {
  async handle(_cli: ConsoleIO): Promise<void> {
    const name = _cli.argument<string>("name");
    _cli.info(`Hello ${name}!`);
    return;
  }
}

Now you can run the command in your terminal

node intent hello vinayak

Read More - Console Docs

Logging

Intent comes with support for file based logging, You can make use of the file based logging. Let's take a quick look,

import { Log } from '@intentjs/core';

const logger = Log();

logger.debug('hello world!');
logger.verbose('verbose');
logger.info('info');
logger.warn('warn');
logger.error('error', e);

Read More - Logging Docs

Exception Handler

Intent comes with global exception filter for your application, along with integration for Sentry enabling you to track and notify you of errors whenever they happen on production.

import { AppConfig } from '@libs/intent';
import { registerAs } from '@nestjs/config';

export default registerAs(
  'app',
  () =>
    ({
      // other config here.

      sentry: {
        dsn: process.env.SENTRY_DSN,
        tracesSampleRate: 1.0,
        profilesSampleRate: 1.0,
        integrateNodeProfile: true,
      },
    }) as AppConfig,
);

Read More - Error Handling

Transformers

Intent comes with support for Transformers which you can use to transform the response objects before you send it out to the client, the clients can also request for additional data on the fly.

You can read more about it here.

Helpers

I have added multiple helper methods which I feel would be very useful, so that you don't have to pollute your code with small logic.

For example, let's take a look at Numbers helper

import { Num } from "@intentjs/core";
Num.abbreviate(1200, { precision: 2 });
// 1.2K

Num.abbreviate(1200, { locale: "hi" });
// 1.2 हज़ार

It also has String helpers, for example.

import { Str } from '@intentjs/core';

Str.pluralize('child');
// children

Str.remove("New OSS NodeJS Framework", "OSS ");
// New NodeJS Framework

If you want to pull some keys from nested objects, you can do so

import { Obj } from '@intentjs/core';

const obj = {
  firstName: "Vinayak",
  lastName: "Sarawagi",
  email: "vinayak@tryintent.com",
  wishlist: [
    { id: 1, name: "Product 1" },
    { id: 2, name: "Product 2" },
  ],
};

Obj.pick(obj, ["firstName", "lastName", "wishlist.*.id"]);
/**
 *  {
 *    firstName: 'Vinayak',
 *    lastName: 'Sarawagi',
 *    wishlist: [ { id: 1 }, { id: 2 } ]
 *  }
 */

Similarly, you can also use Array helpers, let's say you want to return the array but without some keys.

import { Arr } from '@intentjs/core';

const goats = [
  { name: 'Saina Nehwal', sport: 'Badminton' },
  { name: 'Sunil Chetri', sport: 'Football' },
  { name: 'Rohit Sharma', sport: 'Cricket' },
  { name: 'Virat Kohli', sport: 'Cricket' },
];

Arr.except(goats, ['*.sport']);
/**
  [
    { name: 'Saina Nehwal' },
    { name: 'Sunil Chetri' },
    { name: 'Rohit Sharma' },
    { name: 'Virat Kohli' }
  ]
*/

Read more - Helpers Doc

Internationalisation

If you would like integrate localisation in your application, you can easily do so by just defining your lang_code.json file inside resources/lang directory.

Now you can request for translated strings using __ helper.

import { __ } from "@intentjs/core";

__("quote");
// If your dreams do not scare you, they are already becoming a reality.

__("quote", "hi");
// अगर आपके सपने आपको नहीं डरा रहे हैं, तो वो पहले से पुरे होने लग चुके हैं।

Localisation also supports parameterisation, pluralisation out of the box, you can read more about it here.

Support

Though I am currently using Intent in multiple products, it's still the first release, so honestly there will be a few hidden bugs. If you run into any similar issues, you can either raise an issue here, or drop me an email at hi@tryintent.com for any support.

Licence

Intent is licensed under MIT © HanaLabs