Icon

Charles Crete

My perfect Node server for Web APIs

July 18, 2020

When starting a new project, I consider which technologies to pick. This can be a struggle, with so many choices of languages, libraries, and tools. In this article, I'll be writing about a stack I've settle on for writing back-end web APIs, and the process of choosing each component.

We'll only be considering JavaScript (with TypeScript) as it:

  • Is mature and has a large ecosystem
  • Has a good balance of safety (with TS) and performance
  • Is widely supported
  • Is well known and understood by me and my team (the most important point here)

We'll be looking at the choices of:

  • InversifyJS (inversion of control/dependency injection library)
  • Routex (router, built by yours truly)
  • Prisma (database access/"ORM")

I have found that these tools work very well together, and provide a fast, safe, and easily testable way of writing Node API servers.

InversifyJS

The #1 reason for going with a dependency injection library for my projects is the benefits it provides when scaling a project and making testing efficient and correct.

Let's take these 2 pieces of code, which do the same job:

import { prisma } from "./prisma";

export async function createPost(title: string) {
  return await prisma.posts.create({
    data: {
      title,
    },
  });
}
import { inject, injectable } from "inversify";
import { Types } from "./types";

@injectable()
export class PostRepository {
  constructor(
    @inject(Types.Prisma)
    private readonly prisma: PrismaClient
  ) {}

  createPost = async (title: string) => {
    return await this.prisma.posts.create({
      data: {
        title,
      },
    });
  };
}

Testing is best when all side-effects are isolated, therefore for the first example, we must mock the Prisma import. For the second example, we can easily pass a mock object for the Prisma dependency.

const createPostMock = jest.fn();
jest.mock("./prisma", () => {
  return {
    posts: {
      create: createPostMock,
    },
  };
});

it("creates a post", async () => {
  await createPost("my title");

  expect(createPostMock).toHaveBeenCalledWith({
    data: {
      title: "my title",
    },
  });
});
it("creates a post", async () => {
  const prisma = {
    posts: {
      create: jest.fn(),
    },
  };

  const postRepository = new PostRepository(prisma);
  await postRepository.createPost("my title");

  expect(prisma.posts.create).toHaveBeenCalledWith({
    data: {
      title: "my title",
    },
  });
});

They are many other benefits of using dependency injection. You can read more about SOLID for a better explanation than I can produce.

Routex

Using Inversify, we want our router to be integrated with the dependency injection for simplicity and making our controllers/handlers easier to test.

Express has such bindings, however, Express is now very old and still has limited support for newer JavaScript features such a promises, and using it can make it feel like an uphill battle.

This is why I originally created Routex, a modern Node router. It focused on providing a better developer experience using modern features and a clean API. It is still similar to Express, so learning it is painless and swift. Routex also has better error handling and easier support for different response types.

In addition, Routex has Inversify support using a first-party package, enabling tighter integration and simpler usage.

An example of a simple controller would be:

@injectable()
@Controller("/account", [MiddlewareTypes.Authentication])
export class AccountController {
  constructor(
    @inject(Types.Prisma)
    private readonly prisma: PrismaClient
  ) {}

  @Post("/password")
  public async editPasswordHandler(ctx: ICtx) {
    // ...

    return new JsonBody({});
  }
}

Prisma

Prisma feels like a breath of fresh air coming from other ORMs like Sequelize or TypeORM. I had previously migrated to using raw SQL for database access in the past years due to my dissatisfaction with past solutions.

Prisma works as such:

  • You provide it a Prisma schema
  • It generates the required migrations (still experimental)
  • It generates a custom database access client based on your schema

With the generation of types with the client, this means you can have proper type safety from your database, for free. This has helped me catch many bugs or invalid queries in development (TypeScript won't even let you compile!), and helps me ship with greater confidence.

Conclusion

In addition to the libraries mentioned above, these are some other must-haves development tools:

  • Prettier: auto-formats you code, avoiding conversations about which style is better (I usually don't care as long as it formats it itself)
  • ESLint: linting can help you catch some low-hanging bugs and helps with overall code quality

I have found that this particular set of libraries has helped me develop faster with more focus on proper testing and higher code quality, which reduces the number of production issues and improves my overall code quality.

This is by no means the perfect server, just what I have found that works the best for myself and my team.

About me

I am Charles Crete, I work with JavaScript/TypeScript (Node, React), Flutter, and many other technologies.

Follow me on Twitter!