@phnq/service

1.7.0 • Public • Published

@phnq/service

CircleCI

npm version

TL;DR - jump to Getting Started for a barebones example.

Microservices Made Easy

The pros and cons of microservices as an architectural pattern are an oft debated topic in software engineering circles. One ubiquitously shared opinion is that ease of implementation is not in the pros column. The @phnq/service library aims to take some of the pain out of getting started with microservices by providing utilities that deal with the basic plumbing. There are plenty of other benefits too such as end-to-end type safety as scalability.

What are microservices?

Microservices are a way of breaking up a large application into smaller, more manageable pieces. Each piece is a self-contained service that communicates with other services via a network protocol. The services are typically deployed in separate processes or on distinct machines. -- GitHub Copilot

But lets just call them services.

Features

  • Ease of use - the power to weight ratio is high, meaning you can do a lot with very little code.
  • Type safety - end-to-end type safety throughout the system, from backend to frontend, services, clients, etc.
  • Performance - internal communication with pub/sub (NATS) means very few bottlenecks. The included WebSocket API service is also very performant.
  • Scalability - services can be deployed in separate processes or on distinct machines. Automatic load balancing between multiple instances of a service.
  • Flexibility - services can be deployed separately, together, or in any combination. This makes local development easy because a production environment replica is not necessary.

Overview

Before we get into the details, here's a quick overview of the basic moving parts and actors in @phnq/service.

Service

A service is really quite simple. It is merely a collection of handlers associated with a domain.

  • handler - a named function that receieves a single argument (the payload) and returns some result.
  • domain - a string that identifies the service.

If this sounds like a web server, that's because it's more or less the same thing semantically. It's different under the hood, but the idea is the same. A service is a server that handles requests.

Service Client

A service client provides a way to interact with a service. A service client is also associated with a domain, but instead of handling requests, it makes requests to a service with the same domain name. A service client's programmatic API methods are named after the handlers of the service it interacts with.

Web Server/REST API Integration

Service clients can be used within a web server application like an Express app. Side requests can be made to services while the HTTP server handles requests.

Alt text

API Service (WebSocket)

An alternative architecture to the traditional web server one involves having all API communication happen over a WebSocket. In this case, the service clients can be used in a web browser. This architecture has many advantages which will be outlined below.

Alt text

Note: there is an ApiService utility included in @phnq/service.

Getting Started

Here's a barebones example of how to use @phnq/service to create a service and a client for that service.

Run NATS

You will need a NATS server running. You can run one locally with Docker:

docker run nats

Create an API interface

This interface will be used by both the service and the client.

interface GreetingsApi {
  greet: (name: string) => Promise<string>;
}

Create a Service

Use the interface created above to define the service's handlers.

import { Service } from "@phnq/service";

const greetingsService = new Service<GreetingsApi>('greetings', {
  handlers: {
    greet: async (name: string) => {
      return `Hello, ${name}!`;
    },
  },
});

await greetingsService.connect();

Note: the last statement should be wrapped in an async function unless you're using Bun which supports top-level await.

Create a Service Client

Use the same interface again to create a client for the service.

import { ServiceClient } from "@phnq/service";

const greetingsClient = ServiceClient.create<GreetingsApi>('greetings');

const greeting = await greetingsClient.greet('World'); // Hello, World!

That's it for a very basic example of inter-service communication. Next we'll look at how to communictate with a service from a web browser over a WebSocket.

Use the ApiService to create a WebSocket server

The ApiService is a WebSocket server that acts as a gateway or proxy to your services from a web browser.

import { ApiService } from '@phnq/service';

const apiService = new ApiService({ port: 5555 } );

await apiService.start();

Note: Again, the last statement should be wrapped in an async function unless you're using Bun which supports top-level await.

It's a bit magical in that it is semantically isolated from the rest of the system. This is convenient for scalability; you can have as many of these as you want behind a load balancer.

Create an ApiClient

This is similar to the ServiceClient we created above but you can use it in a web browser. The same GreetingsApi interface is used again.

import { ApiClient } from '@phnq/service/browser';

const greetingsClient = ApiClient.create<GreetingsApi>('greetings', 'ws://localhost:5555');

const greeting = await greetingsClient.greet('World'); // Hello, World!

WebSocket vs REST

It's a bit surprising that WebSockets are not more widely used for frontend/backend API communication. Presumably, this is because WebSockets are so low-level that you have to build a lot of infrastructure around them to make them useful; you basically have to invent your own protocol. However, the performance benefits are undeniable, making the dearth of WebSocket-based API utilities all the more remarkable.

Request/Response

The semantics of request/response are really useful; the client wants something and asks for it, then the server responds in kind. This is how the web works, and it's how most APIs work. REST (via HTTP) has this built right in to the protocol.

WebSockets, on the other hand, don't do request/response by default, but it's totally possible to build a request/response protocol on top of WebSockets. This is what @phnq/service does.

The HTTP Bottleneck

The problem with HTTP servers is that every client/server interaction uses a TCP connection. This isn't such big deal when responses are quick. But suppose you have a really slow response (maybe a slow database query or something) that takes, say, 20 seconds. The TCP connection is tied up for these 20 seconds, even though it's doing nothing; the web browser will have to use another connection to make another request. Web browsers typically have a limit of 6 connections per domain, so if you have a lot of slow requests and the browser reaches this limit, the 7th will have to wait for one of the previous requests to complete. Even if responses are quick, the TCP connection is still tied up for the duration of the request/response cycle. This response latency adds up, reducing the overall throughput of the web server.

How WebSockets Solve the Problem

WebSockets are bi-directional (or full-duplex), meaning that communication can be initiated from either the client or the server. When a client sends a message to a WebSocket server, the connection is immediately freed up to do other things. The server can eventually "respond" by sending a message to the client. The slow response scenario is not a communication bottleneck because the connection is only being used when messages are being sent. If the client makes 100 requests that each take 20 seconds to respond, the 100 responses will all come back in 20 seconds, only using a single TCP connection. The same scenario with an HTTP server would take over 5 minutes, occupying 6 connections the whole time!

Usage

TBD

Readme

Keywords

none

Package Sidebar

Install

npm i @phnq/service

Weekly Downloads

78

Version

1.7.0

License

ISC

Unpacked Size

132 kB

Total Files

59

Last publish

Collaborators

  • pgostovic