- Published on
Introducing Intent
- Authors
- Name
- Vinayak Sarawagi
- @vinayak2506
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
- Database Integration
- Using Storage
- Message Queues
- Mailers
- Caching
- Console Commands
- Logging
- Exception Handler
- Transformers
- Helpers
- Internationalisation
- Support
- Licence
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.
Integration | Drivers |
---|---|
RDBMS | MySQL, Postgres, Sqlite |
Storage | Unix File Systems, S3 |
Message Queues | AWS SQS, Redis |
Mailers | SMTP, Mailgun, Resend |
Caching | Redis, In-Memory |
Console Commands | |
Logging | File Based Logging |
Exception Handler | Sentry Integration |
Validations | |
Transformers | |
Helpers | Number, 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,
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.