Developing professional API with Azure Function Typescript + Boilerplate
safwanmasarik
Posted on March 1, 2023
The article here is aimed to give details about 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 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
-
.vscode
contains extension recommendations, debug launch settings, build tasks and default IDE settings. Note that IDE settings inworkspace.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
orqueueTrigger
. - Shared folder contains the core layers such as services (business logic), data layer, modelling and helpers.
Framework and workflow
- The adopted framework follows a
domain-driven approach
, with separate layers forAPI
,service
, anddata
. - First entry point is the
API
layer which will call theservice
layer and receive the api response to be returned. - The
service
layer will perform business logic such ascalculations
anddata formatting
. It also handles calling thedata
layer. - The
data
layer is only responsible to retrieve the data. Data retrieval from database or 3rd party API should be conducted here. - Most JSON object will be converted to Typescript class object and the classes, interface and enums are stored in models folder.
- All classes and functions that are deemed as
helpers
is stored in helpers folder. - For
unit testing
, onlyservice
layer and necessaryhelpers
will be tested.Unit testing
should not do an actual call to the data layer therefore data responses must be mocked. -
Data
layer testing will be covered byintegration 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 folderpostman/AzFuncBoilerplate-IntegrationTest.postman_collection.json
. In this post, we'll not cover for this content.
Libraries & Tooling support
Joiful
Request query or body parameters input's will have constraints 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 at 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 comes in the form of JSON object. json2typescript maps JSON objects to an instance of a TypeScript class aka
Deserialization
. - Service layer will benefit having data layer response modelled as it provides consistent class and property for business logic and formatting.
- Intellisense support.
- Easy to change -> If data response property names changes, class would not need to be changed because the library provide 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
- Utilize online converter such as quicktype.io
View & validate JSON data
- Utilize online JSON data viewer and validator such as JSONGrid
How to add new API endpoint
- Make a copy of an api folder.
- To make life easy delete the
__test__
folder inside the new folder. - Rename the new folder and copy the folder name.
- Update the
function.json
,scriptFile
value to the new folder name. - Now go
index.ts
and update the function name to the folder name. - Your new API endpoint is ready. Press
F5
to run indebug
mode and test it. - While in
debug
mode you can adjust the service layer that you want your api layer to call. The changes is watched and will auto-compile on save and server restart will happen automatically. - 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 other's out there, feel free to share it. Comment if you feel something can be improved or added.
Posted on March 1, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
March 1, 2023