- 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
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" ]