Hello 👋 ,  

If you are reading this post, you are interested in contributing to hyper63 the open-source service framework. This post is part of a series of posts that walk through creating a port for hyper63.

hyper63 is a service framework which uses the ports and adapters pattern to provide a common api for a specific service domain. Using the port, you can build several adapters. Each of these adapters is specific to an implementation for the port. For example, a common api for search would be a port and the common features implemented for that port in elastic-search would be considered an adapter. You could also implement an adapter for Meilisearch. The benefit of having several adapters matching a specific specification is the freedom and flexibility to switch between one implementation or another.

What is a port?

A hyper63 port is a specification that adapters need to follow for a given service. The responsibility of the port is to make sure that the adapter follows the rules of the service. The port validates each method on an adapter and makes sure the caller provides the correct arguments and the adapter method returns the correct result type. Currently, all adapter methods return a promise and the port needs to validate that each method returns a promise with a specific object. A validation that checks functions is the perfect candidate for such a job and we have chosen zod a NPM validation library that parses the data to properly validate it.

Validation Library

zod is a validation library that parses and not validates. It is a great fit for this purpose, because it is declarative and designed to model typescript types.

import * as z from 'zod'

const schema = z.object({
  echo: z.function()
    .args(z.string())
    .returns(
        z.promise(
            z.string()
        )
    )
})

schema.parse(adapter)

With this example, you can see how straight forward and chain-able the zod api is designed. You can dig more into zod here: https://github.com/colinhacks/zod/tree/v3

Designing our port

For this example, we are going to create an echo port, the port takes adapters that have one method, called echo and the adapter is responsible for implementing this method. When the method is called the adapter returns the data that was given to it. This port and adapter is absolutely useless, but should give us enough of a challenge to see how you would go about extending hyper63.

{
  echo: v => Promise.resolve(v)
}

Super Fancy!

Getting Started

In order to follow along in this tutorial, you should have at least v14 of NodeJS - https://nodejs.org installed, a code editor, checkout vscode - https://code.visualstudio.com/.

Creating a port project

Now that we have our design, we are ready to create our port project, which needs to be an NPM module.

mkdir hyper63-echo-port
cd hyper63-echo-port
yarn init -y
touch index.js
touch index_test.js
yarn add zod@next
yarn add -D tape

Lets make this project an ESM project!

In the package.json file, add a type property called type with a value of module.

{
  ...
  "type": "module"
  ...
}
The type property tells NodeJS that all the .js files are in ESM mode.

Index.js

Open the index.js file in a code editor and add the following:

import { default as z } from 'zod'

export default function (adapter) {
  const schema = z.object({
    echo: z.function()
      .args(z.string())
      .returns(z.promise(z.string()))
  })

  let instance = schema.parse(adapter)
  instance.echo = schema.shape.echo.validate(instance.echo)
  return instance 
}
NOTES: we are calling parse because we want an error to through if the adapter does not meet the shape of the schema. Also, we are calling the validate method to wrap the adapter method, this will give us run time validation of the function arguments and return results.

Testing with tape

All ports should have at least one happy path and one sad path test to make sure our port is doing what it should be doing.

open index_test.js in your code editor.

import { default as test } from 'tape'
import port from './index.js'

test('verify port', async t => {
  const adapter = {
    echo: function(s) { return Promise.resolve('Hello ' + s) }
  }

  const a = port(adapter)
  const res = await a.echo('World')
  t.equal(res, 'Hello World')
  t.end()
})

test('throw error', async t => {
  const adapter = {
    ehco: s => Promise.resolve('Hello ' + s)
  }
  try {
    let a = port(adapter)
  } catch (e) {
    t.ok(true)
  }
  t.end()
})

Run our tests

node index_test.js

✨Success! ✨

Github Action

Let's create a Github action to automatically run our test on every push, just to make sure we have some continuous test coverage.

create a new folder .github/workflows

mkdir -p .github/workflows
touch .github/workflows/test.yml

open .github/workflows/test.yml in your code editor

name: test echo port
on: push
jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [14.x]
    steps:
      - uses: actions/checkout@v2
      - name: Use NodeJS ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}
      - run: yarn
      - run: yarn test
        env:
          CI: true

open package.json in your code editor

{  
  ...
  "scripts": {
    "test": "node index_test.js"
  }
}

Readme

create a README.md file

# README

hyper63 echo port

This port is used in hyper63 to support echo adapters

## Example

``` js
import echoPort from 'hyper63-port-echo'
import greetings from 'hyper63-adapter-greetings'

export default {
  app: express,
  adapters: [
    { port: echoPort, plugins: [greetings()] }
  ]
}
```

Deploy to Github

git init
echo node_modules > .gitignore
git add .
git commit -m 'first commit'
gh repo create hyper63-echo
git push origin master
Check Github actions and make sure your tests passed.

Publish to NPM

Make sure you have an account on npmjs.com

modify the package.json file to use your @acct name plus hyper63-port-echo  for the package name

{
  "name": "@acct/hyper63-port-echo"
  ...
}

Then run the NPM publish command

npm publish --access public

You can also setup a Github action to publish your port.

touch .github/workflows/publish.yml
name: Publish Port Cache
on:
  push:
    branches:
      - master
    
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v1
        with:
          node-version: "14.x"
          registry-url: "https://registry.npmjs.org"
          scope: "@acct"
      - run: yarn
      - run: npm publish --access public
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
You will need to setup your NPM TOKEN in your project secrets key value store.

Summary

This concludes the first part of the hyper63 contributing the guide. In this tutorial, we learned how to create a port for hyper63. In the next tutorial, we will learn how to create an adapter for this port.