How to Write Microservices in TypeScript

Explore microservices in other languages

Writing Microservices in TypeScript

Ever since the release of Node.js, JavaScript (and TypeScript in particular) has been an amazing choice for creating microservices

The language itself provides a very agile developer experience, and the asynchronous I/O provided by JavaScript runtimes simply smashed the performance of many other web servers without even trying.

However, JavaScript alone can sometimes be “not enough” given its lack of type-safety. This is where TypeScript (probably Microsoft’s most significant contribution to the JavaScript ecosystem) comes into play.

In this article, we’re going to review why building microservices with TypeScript is such a great idea, what some of the “cons” of doing it (there is no perfect language after all) are, and we’ll also review a quick sample API for you to understand how to get started.

So let’s get cracking!

Decoding Microservices: Best Practices Handbook for Developers

Learn how to overcome microservices challenges by following some best practices

Pros and cons of building microservices with TypeScript

While there is no single  programming language  that is perfect for building microservices, there are always “pros” and “cons” we can assess while picking the right stack for our project.

Let’s take a look at some of the benefits of choosing TypeScript as our microservices  language:

  • Static Typing: Probably TypeScript’s most relevant feature, and the reason why most developers will pick it up instead of using plain JavaScript. The static typing in TS will prevent many errors from reaching the runtime by catching them at compile-time.

  • Async I/O: While asynchronous I/O is more on the JavaScript side rather than a sole TypeScript benefit, it also applies (given how TS runs on top of JavaScript’s runtime). The Async I/O feature provided by these runtimes can improve the efficiency of I/O operations out-of-the-box. In particular, in the context of microservices, you’re able to create a web server capable of dealing with 70k requests per second, without having to do anything special. This alone is one of the main reasons why people chose TS/JS for this task.

  • Fantastic documentation: The documentation site for TS is, hands down, one of the best and most complete sets of documentation. We gotta give credit to the TypeScript team on this one, they’ve been keeping the site updated and filled with details and examples since day one. This simplifies the task of picking up TS as a main language quite a lot!

  • Type-safety across the wire: Thanks to TS’s type definition, you can share it and use it both on the server and client, automatically ensuring that the communication between both sides is safe (from the data types POV). This simplifies the communication and ensures that both sides are speaking “the same language” without the need to add extra checks or validations.

  • Rich Ecosystem: TypeScript benefits from the vast JavaScript ecosystem, which includes a wide range of libraries, frameworks, and tools that can be leveraged when building microservices.

When it comes to building microservices with TS there are really no specific “cons”. However, the language itself does have some downsides when compared to others, and that’s what we’re going to focus on next.

  • Added compilation step: Currently, the most common runtime for building microservices with TS is Node.js, and it only understands JavaScript, so if you’re building with TypeScript, then you’ll need to configure an extra compilation step before you can actually execute your code. This adds a bit of complexity and the need for extra tooling. Other runtimes like Deno treat TS as a “first-class citizen,” but those are not replacing Node as an industry standard.

  • Tooling and library dependency: While the language itself provides you with a standard library capable of building microservices, it is also true that the provided tools are too “low level”. In other words, if you don’t use external libraries, you’ll have to code many basic functionalities that are simply not provided by the language. This causes a dependency on third-party libraries, which the JS/TS community is quite keen on providing.

  • Learning curve: While TS’s documentation is quite extensive, migrating into a type-safe language can be hard specifically for JS developers. This adds extra ramp-up time when the team is switching technologies and can lead to incorrect or slower implementation times while the team is learning.

As you can see, the list is not long, and while there might be other subjective points to add here, they would not be specifically related to microservices, but rather, the language itself.

This is what makes TypeScript (or JavaScript in general) one of the best choices for building microservices.

Let’s now review an example of microservices in TypeScript.

Building a microservice in TypeScript

In this hands-on example, we’ll create a simplified product catalog microservice using TypeScript in Node.js. This microservice will expose RESTful endpoints for managing product information.

Step 1: Project setup

There are many options out there to create a working RESTful API with TypeScript, but we’re going to go with a very popular one: NextJS.

The first step is to create the project using the following line (assuming you have Node.js v18+ installed already):

npx create-next-app@latest –typescript

This command will set up your project, and once you’re ready go into the new folder and continue with the following steps.

Step 2: Define the data model

We’ll quickly create a new interface for our “product” data model that looks like this (in other words, it’ll have an ID, a name, and a price):

				
					export interface Product {
  id: string;
  name: string;
  price: number;
}

				
			

We’ll be using this interface across the rest of our code to properly define the “shape” of our data.

Step 3: Implement RESTful endpoints

We’re using NextJS v13 here, so we’ll take advantage of the new App router, which means there will be two files:

  • app/api/products/route.ts: This file will tackle the POST and GET methods to the /api/products URL
  • app/api/products/[id]/route.ts: This file will tackle the GET requests when we want the details of one particular product (i.e /api/products/1)

Through our API routes in NextJS, we export functions named after the HTTP verb they’ll handle, so on the first one, we’ll have:

				
					import type { NextApiRequest, NextApiResponse } from 'next'
import { Product } from '@/models/product'
import { productsStore } from '@/store/products';
import { NextRequest, NextResponse } from 'next/server';


export async function POST(req: NextRequest, res: NextApiResponse<Product[]>) {
    const jsonBody = await req.json()
    const product = (jsonBody as Product);
    productsStore.push(product);
    return NextResponse.json(productsStore);
}

export function GET(req: NextRequest, res: NextApiResponse<Product>) {
    return NextResponse.json(productsStore)
}

				
			

This means the POST function is receiving a JSON payload on the request’s body and transforming it into an actual Product object. We then add our new product to the store.

For the GET request, we’re just turning our data storage object into JSON, we’ll look at that in a minute.

We have the following code for the second file where we handle getting the details of one particular product:

				
					import { Product } from "@/models/product";
import { productsStore } from "@/store/products";
import { NextApiResponse } from "next";
import { NextRequest, NextResponse } from "next/server";

interface ErrorResponse {
    message: string
}

type GETREsponseType = Product | ErrorResponse 

export function GET(req: NextRequest, {params}: {params: {id: string}}, res: NextApiResponse<GETREsponseType>) {
    let id = params.id
    const product:Product|undefined = productsStore.find( p => p.id == id)
    if(product) {
        return NextResponse.json(product)
    } else {
        return NextResponse.json({
            message: "Product not found"
        }, {
            status: 404
        })
    }
  
}

				
			

We’re defining a custom type called GETResponseType because we need to let TypeScript know that we may return an actual Product or an error with a message, so we use a type union here to define a single type that can be both.

Let’s now take a look at what the data layer looks like for our TypeScript microservice.

Step 4: Implement the data layer

Now it’s time to add database access code. You can use any DB you want at this point, just make sure you are persisting the data. 

For this example, simply create an in-memory static store. If you’re familiar with the Singleton pattern, you’ll see that it is implemented here to make sure the array where the products are kept stays unique across multiple instances. Thanks to the OOP-oriented nature of TypeScript, implementing design patterns is very easy.

				
					import { Product } from "@/models/product";

class ProductStore {
    static instance: ProductStore|null = null;
    productStore:Product[] = []

    static getInstance() {
        if(!ProductStore.instance) {
            ProductStore.instance = new ProductStore()
        }
        return ProductStore.instance
    }

    push(p: Product) {
        this.productStore.push(p)
    }

    find(cb: (value: Product) => boolean): Product|undefined {
        return this.productStore.find(cb)
    }

    toString() {
        return JSON.stringify(this.productStore)
    }

}

export const productsStore:ProductStore = ProductStore.getInstance();

				
			

The logic is not very complex, since we’re deferring all the complexities to the actual array methods that are already implemented. We’re just wrapping our array inside a Singleton. 

That’s it! You’ve just created a basic product catalog microservice in TypeScript. 

You can expand upon this example by adding more features like authentication, pagination, and validation to suit your specific requirements.

Microservices Orchestration with Camunda

Revitalize your TypeScript microservices architecture with the efficiency of Camunda’s orchestration platform. 

TypeScript’s static typing and compatibility with JavaScript opens the doors to developing scalable and maintainable microservices in a modern application landscape. 

Camunda seamlessly augments this with its capabilities to automate and visualize complex workflows while managing cross-service communication and state persistence. 

Embrace transparency in process status, enjoy collaborative process design, and utilize detailed monitoring – all in line with TypeScript’s development philosophy, which prioritizes both developer productivity and system reliability. 

With Camunda, TypeScript developers can confidently construct and refine a network of services that are both agile and structurally sound.

Curious to see how Camunda can help you with microservice orchestration? Check out this article about the top 6 benefits of setting up an event-driven process orchestration, and then dive right into Camunda with a free account.

Start orchestrating your microservices with Camunda