Develop professional API with Azure Function Typescript (Boilerplate code explained)

Develop professional API with Azure Function Typescript (Boilerplate code explained)

The goal is to provide a feature-rich starting point for any Developer / Team to kick-start their next major project using Azure Function Typescript.

The article here is aimed to give details about the framework, workflow and libraries implemented for GitHub repository Azure Function Node.js Typescript Starter and Boilerplate at https://github.com/safwanmasarik/Azure-Function-Boilerplate-Api.

Highlights of this boilerplate:

  • ⚡️ Azure Function Typescript support

  • ♨️ Hot reload capability — auto compile on save and server restart

  • 🃏 Jest — Configured for unit testing + mocking db response

  • ✨ Linq package — an alternative to lodash, Typescript support for enumerating collections

  • 📏 Mssql package — support for local database

  • 💨 Json2Typescript package — Modelling json object to Typescript object

  • 🤣 Joiful package — Joi for Typescript, validate api parameters with class & @decorators.

  • 📈 Typescript project diagnostics enabled — quickly catch error by compiling on background and displaying error in problems bar.

  • 📏 Auto format on save

  • 🌈 Bracket pair colorizer enabled

  • 🤖 Visual Studio code full support and intellisense.

  • 🦠 Microservice architecture — api & database in separate repository, no ORM.

Back-End API Technology

  • Azure Functions

  • Node.js

  • TypeScript

  • Microsoft SQL Server

The README file in the repository contains enough information for successfully running the project.


Let's get started.

Folder structure

Folder structure

  • .vscode contains extension recommendations, debug launch settings, build tasks and default IDE settings. Note that IDE settings in workspace.code-workspace will override .vscode/settings.

  • Default folder structure for Azure Function Node.js is maintained, so the API functions must reside on the root directory. Naming for the API functions is prefixed with az_* to make all the API functions sorted on top.

  • Examples of API naming based on the function type such as http,timerTrigger or queueTrigger.

    api-naming-example

  • Shared folder contains the core layers such as services (business logic), data layer, modelling and helpers.

Framework and workflow

shared-folder

  1. The adopted framework follows a domain-driven approach, with separate layers for API, service, and data.

  2. First entry point is the API layer which will call the service layer and receive the api response to be returned.

  3. The service layer will perform business logic such as calculations and data formatting. It also handles calling the data layer.

  4. The data layer is only responsible to retrieve the data. Data retrieval from database or 3rd party API should be conducted here.

  5. Most JSON object will be converted to Typescript class object and the classes, interface and enums are stored in models folder.

  6. All classes and functions that are deemed as helpers is stored in helpers folder.

  7. For unit testing, only service layer and necessary helpers will be tested. Unit testing should not do an actual call to the data layer therefore data responses must be mocked.

  8. Data layer testing will be covered by integration test which will make actual api call and actual data retrieval from source. Example of integration test with postman collection for CRUDE operation is available in folder postman/AzFuncBoilerplate-IntegrationTest.postman_collection.json. In this post, we'll not cover for this content.

Libraries & Tooling support

Joiful

Request query or body parameters inputs will have constraints and hence need to be validated. Here's how to do it.

Validation enforcement.

import * as jf from 'joiful';

// Get the request body
const params = new ReqCreateUpdateDeleteShip();
params.ship_name = req?.body?.ship_name ?? null;
params.ship_code = req?.body?.ship_code ?? null;

// Validate request body
const joiValidation = jf.validate(params);

if (joiValidation.error) {
  return {
    is_valid: false,
    message: joiValidation.error.message,
    data: null
  };
}

Declaring validation constraints in class.

import 'reflect-metadata';
import * as jf from 'joiful';

export class ReqCreateUpdateDeleteShip {

  @jf.number().optional().allow(null)
  ship_id: number;

  @jf.boolean().optional()
  is_permanent_delete: boolean;

  @jf.string().required()
  ship_name: string;

  @jf.string()
    .regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, "kebab case ('kebab-case', 'going-12-merry', 'jackson')")
    .required()
  ship_code: string;

  @jf.string().email()
  email: string;

  @jf.string().creditCard()
  credit_card_number: string;
}

json2typescript

  • Responses from the data layer come in the form of JSON object. json2typescript maps JSON objects to an instance of a TypeScript class aka Deserialization.

  • The service layer will benefit from having data layer response modeled as it provides consistent class and property for business logic and formatting.

  • Intellisense support.

  • Easy to change -> If data response property names change, the class would not need to be changed because the library provides JsonProperty decorator which allows easy adjustment of JSON data mapping.

Deserialization.

import { JsonConvert } from "json2typescript";
let jsonConvert: JsonConvert = new JsonConvert();
const queryData: object[] = await data.getShip(params);
let modelledDbData: DbShip[] = jsonConvert.deserializeArray(queryData, DbShip);

Class and property decorators.

  • For class properties to be visible to the mapper they must be initialized, otherwise, they are ignored.

  • They can be initialized using any (valid) value, undefined or null.

import { JsonObject, JsonProperty } from "json2typescript";
import { DateConverter } from "../../helpers/json-converter";

@JsonObject("DbShip")
export class DbShip {
  @JsonProperty("id", Number, true)
  ship_id: number = null;

  @JsonProperty("name", String, true)
  ship_name: string = null;

  @JsonProperty("code", String, true)
  ship_code: string = null;

  @JsonProperty("is_active", Boolean, true)
  is_active: boolean = null;

  @JsonProperty("updated_date", DateConverter, true)
  updated_date: Date = null;
}

Custom converter.

import { JsonConvert, JsonConverter, JsonCustomConvert } from "json2typescript";

let jsonConvert: JsonConvert = new JsonConvert();

@JsonConverter
export class DateConverter implements JsonCustomConvert<Date> {
    serialize(date: Date): any {
        return date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate();
    }

    deserialize(date: any): Date {
        return new Date(date);
    }
}

Convert JSON data into Typescript class

quicktype

View & validate JSON data

  • Utilize online JSON data viewer and validator such as JSONGrid

jsongrid

How to add new API endpoint

  1. Make a copy of an api folder.

    api-01

  2. To make life easy delete the __test__ folder inside the new folder.

  3. Rename the new folder and copy the folder name.

    api-02

  4. Update the function.json, scriptFile value to the new folder name.

    api-03

  5. Now go index.ts and update the function name to the folder name.

    api-04

  6. Your new API endpoint is ready. Press F5 to run in debug mode and test it.

    api-05

  7. While in debug mode you can adjust the service layer that you want your api layer to call. The changes are watched and will auto-compile on save and server restart will happen automatically.

  8. Please note that changing environments such that changing values in local.settings.json will require you to restart the server manually.


Final Thoughts

Thank you for taking the time to read my first public technical article. I am grateful to the wonderful developer community out there for nurturing me, and now, it's my turn to give back. I've enjoyed learning and sharing my experiences, and I hope you find them helpful.

If you liked this article and you think this will help others out there, feel free to share it. Comment if you feel something can be improved or added.