JavaScript came a long way since it was first invented. It used to be an uncomplicated hybrid of Java and Scheme intended to power simple static websites and webservers. Netscape LiveWire was supposed to let developers write backend and frontend in the same language. JavaScript quickly settled for just web browsers, though. Nowadays however, the dream of JS-powered servers is a reality.

Twenty five years after JavaScript's first release its ecosystem is one of the most complicated in the industry. Even its syntax can vary depending on the environment and project setup. This is how to import a renderFile function from the Mustache library:

// In Node 12 (which is the current LTS release) and in older projects that used RequireJS:
const mustache = require('mustache');
const renderFile = mustache.renderFile;

// In Node 13, Node 12 MJS files (which are experimental) and modern Babel projects:
import { renderFile } from 'mustache';

// Except this specific library happens to not be compatible with this syntax so it'd actually be:
import mustache from 'mustache';
const renderFile = mustache.renderFile;

// And in Deno which doesn't have a dependency manager:
import { renderFile } from 'https://deno.land/x/mustache/mod.ts';

On top of that, in web browsers the file extension needs to be specified, while in Node it shouldn't be included. To quote the MDN web docs:

Certain bundlers may permit or require the use of the extension; check your environment.

Contrary to what it may seem like, this loose approach to syntax became one of the JavaScript's most important powers.

Choose your own syntax

Ecmascript standard evolves quickly. Faster than web browsers are able to adjust. They take time to adapt to new features and so the Babel transpiler was introduced as a way to let developers use modern JavaScript in browsers which usually stay behind.

This allowed JavaScript to go beyond the Ecmascript standard. Babel is now even used by frameworks to create kind of a "DSS", that is Domain Specific Syntax. One of the most popular libraries, React, popularized a custom syntax called JSX. Every developer has seen it already. And yet it's not actually part of the language.

By configuring Babel with the right plugins JavaScript could look like this:

function handleEvent(event = throw new Error("required")) {
  event.promise
    |> await #
    |> throttle(#, this.throttleValue)
    |> console.log
}

const handler = {
  throttleValue: 1_000
}

handler::handleEvent(someEvent)

This example uses Babel plugins for Smart Pipes Proposal, Throw Expression Proposal, Numeric Separator Proposal, and Function Binding Proposal.

None of these is an official part of Ecmascript, yet. All of them (and many more) can already be used in projects, though. Thanks to Babel. Of course most of them aren't used in production because they're still experimental. Most of them.

Java-like flavour

JavaScript seems to slowly drift away from being a cohesive language and instead becomes more of a base for creating its supersets. A syntax buffet. This is both a curse and a blessing. It makes it more complex to master but, on the other hand, allows for an immense amount of flexibility, design patterns, and paradigms.

Thanks to all that, JavaScript can be made to write and feel almost like Java or C#. In particular, it's possible to create services in a Spring-like fashion.

The first puzzle piece - Metadata

One of the most important pillars of OOP patterns is declaring metadata. In Java it's called annotations. C# has attributes. JavaScript can achieve similar effects using decorators. They work differently under the hood but are usually used for the same purpose:

@handler
class EventHandler {
  @debounce(100)
  handle(event) {
    // ...
  }
}

Decorators are still at the proposal stage but are already widely used in a lot of frameworks and libraries. In fact, the community was so quick to make use of decorators that they now have two implementations in Babel: one following the original proposal and the second based on the revised specification.

The second puzzle piece - Encapsulation

There are multiple ways to achieve encapsulation in JavaScript. Especially, private properties in classes can be done like this:

  • classic underscore notation: variables _likeThis are private by convention
  • class field proposal: variables #likeThis are actually private
  • TypeScript superset: encapsulation using keywords similar to most OOP languages

Even though the third option gives the most features and a familiar syntax the choice isn't that obvious. The Class Field Proposal is already at Stage 3 which in practice means it'll soon become part of the Ecmascript standard in the form it is now. It is also the only way to make the fields actually incaccesible outside of the class at runtime. Private fields in TypeScript can still be (accidentaly) accessed by various methods, e.g. with Object.entries() function.

On the other hand, TypeScript has clearer and more versatile syntax for encapsulation. A field can be explicitly set as public, private, readonly (final) and even protected. It also supports the static keyword.

Of course, as usual with JavaScript, both the # notation and TypeScript access keywords can be used at the same time. Most codebases seem to stick to just TypeScript, though. It looks like this:

class DbConnector {
  private readonly db;

  constructor() {
    this.db = new Connection();
  }

  protected doSomething() {
    this.db.doSomething();
  }
}

The third piece - Type checking

Again, there are multiple ways to bring type checking to JavaScript:

  • Flow: static code analyzer
  • TypeScript: syntax sugar for compile time static typing
  • other library specific aproaches, like prop-types for React

This time the choice is more obvious. Flow can be used to easily add type checking to existing code bases without modifying them but TypeScript is currently a de facto standard. Its type definitions are used by IDE's and code editors to provide autocompletion even if the project itself doesn't use TypeScript. It also, as already mentioned, brings access modifiers to the table. A simple static typing example:

function deepCopy<T extends Object>(obj: T): T {
  const serialized = JSON.stringify(obj);
  return JSON.parse(serialized) as T;
}

const person = {
  age: 27,
  name: 'Lorem',
};

const person2 = deepCopy(person);

person2.name = 42; // error, cannot assign number to `name`

It's clear from this snippet that type checking in TypeScript is advanced. In fact, it's a lot more powerful than in Java or C#. It's made so that it doesn't limit the dynamic nature of the language. Most of its features like partial types or excludes aren't used often. Non-nullable types and interfaces are a lot more handy, though.

It's important to remember that TypeScript is just a syntax sugar. Code like this:

interface Connector {
  connect: () => boolean;
  closeConnection: () => void;
}

class DbConnector implements Connector {
  private readonly db: Database;

  constructor(url: string) {
    this.db = new Database(url);
  }

  // note there is no `override` keyword, unfortunately
  connect(): boolean {
    this.db.connect();
    return true;
  }

  closeConnection(): void {
    this.db.close();
  }
}

Will be stripped of types, interfaces and accessors during compilation and will result in the following code:

class DbConnector {
  constructor(url) {
    this.db = new Database(url);
  }

  connect() {
    this.db.connect();
    return true;
  }

  closeConnection() {
    this.db.close();
  }
}

There is no type information at runtime. The interface is gone, too. Of course, this is still an improvement over vanilla JavaScript without types. And there is a proposal for this. Of course, as with any proposal, it can already be used.

Putting it all togheter

Now that JavaScript can have static typing, is able to declare metadata on properties, and provides standard syntax for encapsulation and interfaces, it can use the same patterns as Java.

NestJs is a Spring inspired framework for JavaScript. A sample REST API endpoint can be implemented like this:

@Controller('reindeers')
export class ReindeerController {
  private readonly reindeers: Reindeer[] = [];

  @Get()
  findAll(): Reindeer[] {
    return reindeers;
  }

  @Post('new/:name')
  create(@Param('name') name: string) {
    throw new HttpException(`Cannot create a reindeer named ${name}, try later`, HttpStatus.NOT_IMPLEMENTED);
  }
}

The code should look familiar to everyone who has ever used Spring. It defines a controller and its endpoints declaratively with annotations decorators. Same for the path parameter. Returned values and thrown exceptions are automatically serialized into JSON with appropriate HTTP statuses.

Of course, NestJs is much more capable than just simple REST controllers. Some of its features include:

  • modules (which serve a similar role to Java packages)
  • dependency injection
  • built-in GraphQL support
  • OpenAPI/Swagger integration
  • ORM (similar to JPA/Hibernate)
  • Redis, Kafka and other extensions
  • and many other features one can expect from an enterprise framework

Reactive programming

As a cherry on top, NestJS supports ReactiveX patterns with RxJs, a JavaScript equivalent to RxJava. This is a modified example from NestJs docs; an interceptor that transforms all nulls and undefines to empty strings:

@Injectable()
export class ExcludeNullInterceptor implements NestInterceptor {

  intercept(context: ExecutionInterceptor, next: CallHandler): Observable<any> {
    return next
      .handle()
      .pipe(
        map(value => value ?? '')
      );
  }
}

Summary

There is a lot of factors to consider when comparing NestJs with Spring. Some of them may include:

  • TypeScript is less verbose than Java and provides null safety. In fact, it's more fair to compaire it to Kotlin.
  • NestJs is easy to learn, especially for people with TypeScript or Angular expierience. Frontend developers should be able to code simple backends with ease.
  • Although NestJs is designed to handle large projects, the fact that Spring has been battle tested for 17 years can't be ignored.
  • NestJs runs on Node. Of course Node vs JVM argument isn't a clear win for any of the two and really deserves its own article.
  • With NestJs on backend, knowledge sharing between backend and frontend devs can become more meaningful, especially since TypeScript can also be used on frontend (e.g. with React)
  • NestJs has a lot of modern solutions more closely integrated with the core framework than Spring. Especially, GraphQL APIs are treated with almost the same priority as REST APIs.
  • While NestJs has dedicated security features, Spring Security is still more advanced.

Overall, while NestJs won't replace Spring any time soon, it is a valid alternative to consider. It can be a perfectly viable option for simple services and middlewares that need to expose a REST or GraphQL API.