michaelAwad.io

Building a Typescript NodeJS backend API using FeathersJS

...

March 09, 2021

An exotic option for a NodeJS backend is FeathersJS. It’s a lightweight framework on top of nodeJS and Express. The key features are its real-time socket.io, a CLI where you can generate services that give you all REST options for free, and generation of authentication. Also, it has a unique workflow. Without further ado, let’s dive into it.

Before we start

There is some basic knowledge required for this article to be of any use to you:

  • You need to now basic JavaScript and Typescript
  • Basic understanding of HTTP methods for restful APIs
  • Basic understanding of Express
  • Being familiar with Postman

Let’s get started

Let’s start with installing feathers globally.

npm install @feathersjs/cli -g

Now we can generate the app:

feathers generate app

The Feathers CLI will ask a few questions:

  • Make sure you select Typescript
  • Select REST and Realtime via Socket.io.
  • Check yes when asked if the app uses authentication and choose the Username + Password (local) options.
  • Choose Mongoose as a service.

Generate our services

What is a Feathers service? You generate a restful endpoint meaning you get all HTTP methods for restful APIs for free.

Overview of the restful HTTP methods:

HTTP Verb CRUD Description
GET Read Find all data (potentially matching a query) or get a single data entry by its unique identifier
POST create Create new data
PUT Update/Replace Update an existing data entry by completely replacing it
PATCH Update/Modify Update one or more data entries by merging with the new data
DELETE Delete Remove one or more existing data entries

The beauty of it is that it is protocol-independent. So it can be called through a rest API, via WebSocket, or internally within the application.

Let’s generate our services using the cli:

feathers generate service

Again you will have to answer some questions, most important is that you name it learning-modules.

Setup your service

Your service will be found in the services folder, in this case, in a learning-modules folder. It consists of the following files:

  • learning-modules.class.ts

    • It contains your service definition.
  • learning-modules.hooks.ts

    • The file where the business logic will live
  • learning-modules.service.ts

    • The service registration

One file is created outside of this folder and within the models folder. in this case: learning-modules.model.ts

Here we can find our mongoose model, which we will need to set up based on our data model.

You can use this as our learning-module data schema and model:

;({
  title: { type: String, required: true },
  chapters: [
    {
      title: { type: String, required: true },
      description: { type: String, required: true },
      vimeoId: { type: String, required: false },
      started: { type: Boolean, required: true },
      free: { type: Boolean, required: true },
      completed: { type: Boolean, required: true },
    },
  ],
  summary: { type: String, required: true },
  thumbnail: { type: String, required: false },
  started: { type: Boolean, required: true },
  free: { type: Boolean, required: true },
  users: [
    {
      userId: { type: String, required: true },
      progress: { type: String, required: true },
    },
  ],
},
  {
    timestamps: true,
  })

Check if our service works

Start the service with npm start and check on what port the server is running. Open Postman, go to the user endpoint, and do a post with an e-mail and password.

Use that e-mail and password to post to the authentication endpoint, and you should receive a token.

Now go to the learning-modules endpoint and paste the token in the authorization token bearer.

Do a post matching to the data attributes created in your model. Now do a get call on the same endpoint, and you should see de data you posted earlier.

Did you see the learning module date you posted? Great, the service works!

Did you realize how quickly you created a restful API with authentication?

Workflow

You will see something like this in the before hook of the hooks file.

all: [authenticate("jwt")]

This means that all requests to this service call authentication.

The same mechanism applies when we want to build logic. We need to add the function call in one of those hooks.

Before hooks to be able to work appropriately in Typescript, we need to import the type definitions for the context:

import { HookContext } from "@feathersjs/feathers"

When you create the function, you need to keep a few things in mind: Use a function assignment call Make it async Pass it the context Return the context

For example, we are in the user hooks, which is automatically generated when choosing the authentication option:

const addUserToLearningModule = async (
  context: HookContext
): Promise<HookContext> => {
  context.data.createdAt = new Date()
  const params = {
    query: { free: true },
  }

  const data = { userId: context.result._id, progress: "0" }

  const freeModules = await context.app.service("learning-modules").find(params)

  await freeModules.data.map((item: any) => {
    context.app
      .service("learning-modules")
      .patch(item._id, { $push: { users: data } })
  })

  return context
}

To call the function add it to the hooks like this:

   before: {
    all: [authenticate('jwt')],
    find: [],
    get: [],
    create: [addAllUsersToLearningModule],
    update: [],
    patch: [],
    remove: []
  },

What is happening here?

  1. When you want to save a value in the database, you can overwrite context.data.. So the first thing that happens is that a createdAt timestamp is set using a new date.
  2. We are creating a data variable, using context.result._id. Since we are in the after create hook, we can access the _id of user-created using the post function. And we were hardcoding progress to 0
  3. We are creating a variable that fetches free modules from the database. The cool thing here is we get it from another service. By using: await context.app.service(“service-name”), you can get access to any service you created and, in this case, perform MongoDB queries in them. Here we use find, which takes an object containing the query object, so in this case, it is free: true.
  4. The main goal of this function is to assign the user to all free learning-modules. That data model contains a users array. So we map over the freeModules array that the MongoDB find query returned.
  5. We again grab the service with app.service, and we use the patch function from MongoDB and push the variable we created in step 2 into that array. The first argument of the patch function is the id; the second argument is the data that should be updated.
  6. Since we are dealing with an array within the database, we need to use the $push function. This function takes an object, where the key is the array key, and the value is, in this case, an object.
await context.app.service("service-name")

Inside a function, you can call any service and do with that service what you want, even call MongoDB queries on it!

Different hooks have different ways to handle the context. Check out their extensive documentation on how to use those: https://docs.feathersjs.com/api/hooks.html#hook-context

Conclusion

The subjects we covered are everything you need to start working on your API. It is quick to set up, works with Typescript, it’s robust, flexible, and fast. Once you get to know the workflow, you will start to appreciate it. It has great documentation for it not being ExpressJS, and their responses on GitHub issues are on point and fast. It sounds to me like there is no excuse not to give this exotic a try.


Michael Awad

I'm fascinated by the power of a strong mindset. I combine this with being a web developer, which keeps me motivated. But how, you may ask? That's what I share on this website. For more information about me personally, check out the about me page: About Me

Are you enjoying this blog and do you want to receive these articles straight into your inbox? Enter your e-mail below and hit the subscribe button!



I won't send you spam and you can unsubscribe at any time