JavaScript is one the fastest evolving languages in the world. With the right approach it can serve as a direct alternative to solutions like Java and Spring.
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 null
s and undefine
s 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.
Hero image by Krisjanis Mezulis on Unsplash, opens in a new window