Category
NodeJS

Generating Express.js Boilerplate with Fern API

A fresh look at Fern, a partial open-source API development toolkit, and how it can help you generate boilerplate for your backend server.

Published on
Water Lilies by Claude Monet
Generating Express.js Boilerplate with Fern API
Table of Contents

Overview of Fern API

Fern is an open source toolkit for designing, building, and consuming REST APIs. With Fern, you can generate client libraries, API documentation, and boilerplate for your backend server to reduce the time spent on repetitive and boring tasks.

Fern is fully compatible with OpenAPI. You can use your existing OpenAPI spec to generate code and documentation with Fern. If you want to try something new, you can use Fern's own API spec format.

It is funded by Y Combinator (W23 batch) and Danny and Deep have been working on it since April 2022.

By October 2023, Fern has 1,900 stars on GitHub and 10,000 weekly downloads on npm.

This project looked promising to me, in additon to that, helped me to save significant amount of time to complete a project I am working on. So I decided to write a blog post about it. I hope you will find it useful.

Supported Generators

List of supported libraries and programming languages by October 2023:

Client Libraries and SDKs

  • TypeScript
  • Java
  • Python
  • Go

Server Boilerplate

  • Express.js
  • Spring Boot
  • FastAPI

I have used Express.js generator in this blog post. According to the developers Fern's integration with Express.js is better than other frameworks.

Getting Started

You can install Fern CLI with npm:

npm install -g fern-api

or if you are using bun:

bun install -g fern-api

After installing Fern CLI, move to your project directory and run the following command:

fern init

this will initialize Fern in your project directory. It will create a fern folder in your project directory. This folder will store your API specification and Fern configuration files.

Creating an API Specification

I have not used Fern's own API specification format in this blog post. Instead, I have used an OpenAPI specification, which is more popular way of defining APIs. To use Fern, you can use your existing OpenAPI specification or create a new one. If you want to try Fern's own API specification format, you can convert it to an OpenAPI specification with fern generate command (after making the necessary configurations in generators.yml) whenever you want.

Go inside fern folder and create a new file named openapi.yml inside api/openapi.

fern/
├─ fern.config.json # root-level configuration
└─ api/ # your API
  ├─ generators.yml # generators you're using
  └─ openapi/
    └─ openapi.yml # your openapi spec

Delete if there is a /api/definition directory in your project directory, if you are using an OpenAPI specification.

In this article, I've crafted an API blueprint for a basic feature flag server. If you're unfamiliar with feature flags, they allow you to turn features on or off, or change their values in your software without rolling out a new release. Consider a news app where you wish to adjust the number of articles a user can access per day. With feature flags, you can modify this limit without launching a fresh version, by retrieving the current value of articleLimit from your feature flag microservice. While they're also useful for A/B testing and canary releases, those topics aren't the focus of today's post.

When defining your API with OpenAPI, you should provide a unique operationId for each endpoint. Fern uses operationId to generate functions for the server.

Generating the code

Add needed generators

Run the command below to add Node.js generator to generators.yml file:

fern add fern-typescript-node-sdk

When I was working with the fern add command, I noticed that it is not guaranteed to add the latest version of the generator. Therefore, you can check the latest version of the generator you are going to use on the Fern API GitHub page.

Fern also provides a Postman collection generator, which works better than importing an OpenAPI spec to Postman directly. You can add it to the generators.yml file with the following command:

fern add fern-postman

You can find the full list of generators on Fern Docs.

After adding the generators Node.js and Postman, your generators.yml file should look like this:

default-group: local
groups:
  local:
    generators:
      - name: fernapi/fern-typescript-node-sdk
        version: 0.7.2
        output:
          location: local-file-system
          path: ../../src/api/generated
      - name: fernapi/fern-postman
        version: 0.0.45
        output:
          location: local-file-system
          path: ../../postman

Output location will remain a local directory for this post, but it can also be a GitHub repository or an NPM package.

Change the default organization name

Change the default organization name in the /fern/fern.config.json file. It will be the prefix of your classes.

Login and generate the code

Fern is an open source project, but it has some limitations. When I was writing this blog post, we were allowed to build only one SDK with up to 20 endpoints.

Before generating the code, log in to Fern with the following command:

fern login

Once you are logged in, generate the code with the following command:

fern generate

This command will generate the code and documentation for your API on Fern's cloud. To generate the code locally, use the --local flag. Docker must be installed on your machine. If you tried generating the code locally and it failed, retry on your machine, which may solve the problem.

Using the generated code

If you used the generators.yml file above, the following directory structure will be created after running the fern generate command:

fern/
├─ fern.config.json
└─ api
  ├─ generators.yml
  └─ openapi/
    └─ openapi.yml
src/
└─ generated/
  └─ api/
  └─ core/
  └─ errors/
  └─ serialization/
  └─ ...
postman/
└─ collection.json

Before starting to implement the server, we need to install the dependencies.

Here is the package.json file for this project.

{
  "name": "flags-service",
  "scripts": {
    "build": "tsc",
    "start": "node lib/server.js",
    "dev": "nodemon src/server.ts",
    "format": "prettier --write src/**",
    "format:check": "prettier --check src/**",
    "lint": "eslint",
    "lint:fix": "eslint --fix"
  },
  "dependencies": {
    "cors": "^2.8.5",
    "express": "^4.18.2"
  },
  "devDependencies": {
    "@types/cors": "^2.8.13",
    "@types/express": "^4.17.16",
    "@types/node": "^18.11.18",
    "@typescript-eslint/eslint-plugin": "latest",
    "@typescript-eslint/parser": "latest",
    "eslint": "^8.33.0",
    "nodemon": "^2.0.20",
    "prettier": "^2.8.3",
    "ts-node": "^10.9.1",
    "typescript": "4.6.4"
  }
}

and tsconfig.json

{
  "compilerOptions": {
    "module": "CommonJS",
    "esModuleInterop": true,
    "target": "esnext",
    "moduleResolution": "node",
    "strict": true,
    "rootDir": "src",
    "outDir": "lib",
    "allowSyntheticDefaultImports": true,
    "allowJs": true,
    "resolveJsonModule": true
  },
  "include": ["src"]
}

Run the command below to install the dependencies:

npm install

Or more faster with bun:

bun install

and the other dependencies:

npm install cors

Create a new folder named services under src folder. And create a new file named flags.ts inside services folder.

Now we need to implement FlagsService class. Fern will handle the rest of the things, such as routing and validation.

import { FlagsService } from '../generated/api/resources/flags/service/FlagsService'

export default new FlagsService({
  async create(req, res) {},
  async findbykey(req, res) {},
  async update(req, res) {},
  async delete(req, res) {},
  async findall(req, res) {},
})

You need to implement all of the methods in the FlagsService class. The function names are derived from the operationId field in the OpenAPI spec.

In this post, I will provide mock implementations for all of the methods. You can create real implementations if desired.

Implementing the services

Create /src/database.ts file. We will simulate database operations in this file.

import { Flag } from './generated/api'

export function createFlag(flag: Flag): Flag | Error {
  return flag
}

export function getFlag(key: string): Flag | undefined {
  return undefined
}

To keep things simple, I will not use any real database, including an in-memory database. The function above simply assumes that there is no flag with the given key.

Now return to the src/services/flags.ts file and implement create function:

import { Flag } from './../generated/api'
import { getFlag } from '../database'
import { BadRequestError } from '../generated/api'
import { FlagsService } from '../generated/api/resources/flags/service/FlagsService'

export default new FlagsService({
  async create(req, res) {
    const existing = await getFlag(req.body.key)
    if (existing) {
      throw new BadRequestError('flag already exists')
    }
    const flag = createFlag(req.body as Flag)
    if (flag instanceof Error) {
      throw new InternalServerError('flag could not be created')
    }
    return res.send(flag)
  },
  async findbykey(req, res) {},
  async update(req, res) {},
  async delete(req, res) {},
  async findall(req, res) {},
})

Using the same approach, you can implement the rest of the methods in the FlagsService class.

import { Flag } from './../generated/api'
import { createFlag, deleteFlag, getAllFlags, getFlag, updateFlag } from '../database'
import { BadRequestError, InternalServerError, NotFoundError } from '../generated/api'
import { FlagsService } from '../generated/api/resources/flags/service/FlagsService'

export default new FlagsService({
  async create(req, res) {
    const existing = await getFlag(req.body.key)
    if (existing) {
      throw new BadRequestError('flag already exists')
    }
    const flag = createFlag(req.body as Flag)
    if (flag instanceof Error) {
      throw new InternalServerError('flag could not be created')
    }
    return res.send(flag)
  },
  async findbykey(req, res) {
    const flag = await getFlag(req.params.flagKey)
    if (flag instanceof Error) {
      throw new InternalServerError('flag could not be retrieved')
    }
    if (!flag) {
      throw new NotFoundError('flag not found')
    }
    return res.send(flag)
  },
  async update(req, res) {
    const flag = await getFlag(req.body.key)
    if (!flag) {
      throw new NotFoundError('flag not found')
    }
    const updatedFlag = await updateFlag(req.body as Flag)
    if (updatedFlag instanceof Error) {
      throw new InternalServerError('flag could not be updated')
    }
    return res.send(updatedFlag)
  },
  async delete(req, res) {
    const flag = await getFlag(req.params.flagKey)
    if (!flag) {
      throw new NotFoundError('flag not found')
    }
    const deleted = await deleteFlag(req.params.flagKey)
    if (deleted instanceof Error || !deleted) {
      throw new InternalServerError('flag could not be deleted')
    }
    return res.send({ message: 'flag deleted' })
  },
  async findall(req, res) {
    const flags = await getAllFlags()
    if (flags instanceof Error) {
      throw new InternalServerError('flags could not be retrieved')
    }
    return res.send(flags)
  },
})

and database.ts

import { Flag } from './generated/api'

export function createFlag(flag: Flag): Flag | Error {
  return flag
}

export function getAllFlags(): Flag[] | Error {
  return []
}

export function getFlag(key: string): Flag | Error | undefined {
  return {} as Flag
}

export function updateFlag(flag: Flag): Flag | Error {
  return flag
}

export function deleteFlag(key: string): boolean | Error {
  return true
}

And that's it! There are more types of errors in the API spec you can return, such as UnauthorizedError or ForbiddenError. You can find the full list of errors in the API spec.

Eg.

throw new UnauthorizedError('unauthorized')

Now, simply create a starter file for your server.

Server implementation

/src/server.ts

import cors from 'cors'
import express from 'express'

import { register } from './generated'
import flags from './services/flags'

const PORT = process.env.PORT ?? 8080

const app = express()

app.use(cors())

register(app, {
  flags: flags,
})

app.listen(PORT)
console.log(`🎉 Listening on port ${PORT}...`)

Start the server

Run the command provided to start the server:

npm run dev

Test the server

curl --location 'http://localhost:8080/api/v1/flags'

You should get an empty array as a response.

Bonus: Create a Docker image

FROM node:20-alpine

COPY package*.json ./
COPY src ./src/
COPY tsconfig.json ./

RUN npm install -g nodemon
RUN npm install
RUN npm run build

EXPOSE 8080

CMD [ "npm", "run", "start" ]