Skip to content

A simple serverless counting API with Azure functions in Typescript

License

Notifications You must be signed in to change notification settings

GuilhermeMGBR/ServerlessCounter

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Serverless Counter

A simple serverless counting API with Azure functions in Typescript.

Quality gate status Reliability rating Vulnerabilities Bugs Code smells Coverage

SonarCloud CI status Deploy CI status Health Check status Created by Guilherme Garcia Version License

Index

About the application

A simple serverless counting API with Azure functions in Typescript. Proof of concept and template for greater APIs!

  • The get and hit endpoints are also stateless and can scale as far as your database and wallet allows.
  • The endpoint behaviors (located within a service) can be utilized and deployed using a conventional Node.js server.

Technologies

  • Azure Functions as serverless infrastructure
  • Node.js as cross-platform JavaScript runtime environment
  • MySQL 2 as MySQL client for Node.js
  • Typescript as a strongly typed programming language
  • Zod for type schema validation

Tools

  • Yarn as package manager
  • Babel for high performance javascript compilation
  • Prettier for code formatting and better commits/diffs
  • Jest for testing
  • SQLite for in memory SQL testing environment
  • Sonar for code analysis
  • Husky for git hooks
  • Docker for local testing and simulation
  • Act for local GitHub workflows testing and execution

Getting started

Environment configuration

Create a local.settings.json file inside the ./src folder.

  • Replace {{YOUR_CONNECTIONSTRING}} with the connection string to your MySQL database of choice
{
  "IsEncrypted": false,
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "NODE_ENV": "development",
    "DB_COUNTER_CONNECTIONSTRING": "{{YOUR_CONNECTIONSTRING}}",
    "DB_COUNTER_REJECTUNAUTHORIZED": "true",
    "DB_COUNTER_CA": "{{YOUR_CA}}"
  }
}

To run and debug functions locally, install azure-functions-core-tools on your machine

Installation with yarn:

yarn global add azure-functions-core-tools

=> The 'devcontainer' comes with this preinstalled

Dependencies
  • Open the terminal inside the src folder
  • Install dependencies with yarn
yarn
Recommended VS Code extensions:

Usage

Counters are identified by their namespace/key pair.

  • The namespace must be unique, keys can repeat across different namespaces
  • We can check the current counter value or add 1 to it.

The API endpoints follow this URL structure:

{{BaseURL}}/api/{{Action}}/{{Namespace}}/{{Key}}?code={{Code}}

Where:

  • {{BaseURL}} => URL to where the Azure function app is hosted
  • {{Action}} => get to get the current value or hit to raise the count by 1
  • {{Namespace}} => namespace of the desired counter
  • {{Key}} => key of the desired counter
  • {{Code}} => code required to authenticate requests to a deployed Azure Function
Get current value

Request:

curl -X GET http://localhost:7071/api/get/namespace1/key1?code=ABC --header 'Accept: */*'

Response:

{
  "value": 1
}

Getting the value of a non-existent counter will return 0.

Raise the count

Request:

curl -X GET http://localhost:7071/api/hit/namespace1/key1?code=DEF --header 'Accept: */*'

Response:

{
  "value": 2
}

Raising the count of a non-existent counter will create the counter and raise the count to 1.

Inspect API usage

Lists counter usage data and overall active x deleted status count.

curl -X GET http://localhost:7071/api/usage?code=XYZ --header 'Accept: */*'
OR
curl -X GET http://localhost:7071/api/usage/namespace1?code=XYZ --header 'Accept: */*'
OR
curl -X GET http://localhost:7071/api/usage/namespace1/key1?code=XYZ --header 'Accept: */*'
Inspect all namespace/key pairs

Request:

curl -X GET http://localhost:7071/api/usage?code=XYZ --header 'Accept: */*'

Response:

{
  "activeCounters": [
    {
      "namespace": "namespace1",
      "key": "key1",
      "hits": 123,
      "createdAt": "2023-08-01T01:48:50.000Z",
      "lastHit": "2023-08-01T15:53:14.000Z"
    },
    {
      "namespace": "namespace1",
      "key": "key2",
      "hits": 456,
      "createdAt": "2023-08-01T02:50:50.000Z",
      "lastHit": "2023-08-01T16:55:14.000Z"
    },
    {
      "namespace": "namespace2",
      "key": "key1",
      "hits": 789,
      "createdAt": "2023-08-01T02:48:50.000Z",
      "lastHit": "2023-08-01T16:53:14.000Z"
    },
    ...
    {
      "namespace": "namespace1000",
      "key": "key1",
      "hits": 101112,
      "createdAt": "2023-08-01T04:48:50.000Z",
      "lastHit": "2023-08-01T17:53:14.000Z"
    }
  ],
  "status": {
    "active": "1285",
    "deleted": "25"
  }
}
Inspect all keys from one namespace

Request:

curl -X GET http://localhost:7071/api/usage/namespace1?code=XYZ --header 'Accept: */*'

Response:

{
  "activeCounters": [
    {
      "namespace": "namespace1",
      "key": "key1",
      "hits": 123,
      "createdAt": "2023-08-01T01:48:50.000Z",
      "lastHit": "2023-08-01T15:53:14.000Z"
    },
    {
      "namespace": "namespace1",
      "key": "key2",
      "hits": 456,
      "createdAt": "2023-08-01T02:50:50.000Z",
      "lastHit": "2023-08-01T16:55:14.000Z"
    }
  ],
  "status": {
    "active": "2",
    "deleted": "1"
  }
}
Inspect a single namespace/key pair

Request:

curl -X GET http://localhost:7071/api/usage/namespace1/key1?code=XYZ --header 'Accept: */*'

Response:

{
  "activeCounters": [
    {
      "namespace": "namespace1",
      "key": "key1",
      "hits": 123,
      "createdAt": "2023-08-01T01:48:50.000Z",
      "lastHit": "2023-08-01T15:53:14.000Z"
    }
  ],
  "status": {
    "active": "1",
    "deleted": "0"
  }
}

Development

Running locally

Build and run the App:

This will install the required dependencies, build and start!

yarn start
  • To start without installing dependencies or re-building the app:
yarn start:only
OR
yarn so

=> Remember to follow the environment configuration from the Getting started before running the app!

Manual build

Run the build command:

This will install the dependencies and run a build

yarn build
  • To run a build without installing dependencies:
yarn build:only
OR
yarn bo
  • The build can re-run after each file save in watch mode
yarn watch:build
OR
yarn wb
Linting

Run the lint command:

yarn lint
OR
yarn lt
Formatting

Run the format command:

This will automatically fix errors where possible

yarn format
OR
yarn fmt
  • To check formatting errors without making changes to files:
yarn format:check
OR
yarn fc
Type check

Make sure to have installed dependencies from the initial setup

Run type check:

yarn type-check
OR
yarn tc
  • The type check can re-run after each file save in watch mode
yarn watch:type-check
OR
yarn wtc
Testing

Make sure to have installed dependencies from the initial setup

Build and run tests:

yarn test
  • The test can re-run after each file save in watch mode
yarn watch:test
OR
yarn wt
Sonar analysis

An analysis is run automatically on SonarCloud for open PRs and changes to the main branch.

To run an analysis locally with SonarQube and Docker:

Start a local SonarQube instance:

yarn sonar-server:start
OR
yarn ss
[Optional] Persist analysis results

To persist the analysis results when running a local server of SonarQube:

Create a .env.sonar-server.local file inside the ./src folder

SONAR_JDBC_URL={{YOUR_URL}} # sample: jdbc:postgresql://hostname.com/db_name
SONAR_JDBC_USERNAME={{YOUR_USERNAME}}
SONAR_JDBC_PASSWORD={{YOUR_PASSWORD}}

Replace placeholders with the connection values to your PostgreSQL instance:

  • {{YOUR_URL}}
  • {{YOUR_USERNAME}}
  • {{YOUR_PASSWORD}}

It is possible to run an instance of PostgreSQL inside another docker container!

Start a local SonarQube instance with persistence:

yarn sonar-server:start-persistent
OR
yarn ssp

Sonar Scanner configuration

Set environment variables with sonar server connection details:

  • SVRLSSCTR_SONARQUBE_LOCAL_HOSTURL
  • SVRLSSCTR_SONARQUBE_LOCAL_LOGIN

They can be set inline, before the run command:

SVRLSSCTR_SONARQUBE_LOCAL_HOSTURL=https://your.local.url; SVRLSSCTR_SONARQUBE_LOCAL_LOGIN=sqp_yourTokenXYZ; yarn sonar

Run Sonar Scanner

yarn sonar
OR
yarn s
Pre-commit hook

The pre-commit hook should run automatically before every commit through Husky.

To manually run all pre-commit checks:

yarn pre-commit
OR
yarn pc

This hook does type checking, linting, format checking, runs all tests and checks for blocked terms, stopping and showing errors from the first one to fail, if any.

Pipeline validation

We can run pipeline workflows/ jobs/ steps locally with the help of Nektos/act:

  • Make sure you have Docker installed on your local machine
  • Install Nektos/act
  • Add the secrets required by the chosen pipeline at a .secrets file at this repository's root folder (same folder as the Readme)
  • Open a terminal inside the package.json folder
  • Run the script with the desired pipeline to validate:
    • It will download a docker container and run the pipeline inside it, the first run may take a while!
yarn act:sonarcloud
yarn act:deploy
yarn act:health-check
[optional] Source .secrets to local environment

The secrets will be sourced from a .secrets file at this repository's root folder (same folder as the Readme)

To source local environment secrets on terminal open, add this to your .bashrc or .zshrc:

#
# Allow parent to initialize shell
#
if [[ -n $ZSH_INIT_COMMAND_SVRLSSCTR ]]; then
  echo "Running: $ZSH_INIT_COMMAND_SVRLSSCTR"
  eval "$ZSH_INIT_COMMAND_SVRLSSCTR"
fi

This will trigger a dev-environment-init.sh run when using Visual Studio Code on macOS.

Continuous integration

Code analysis

Automatic code analysis with Sonarcloud. To include code coverage, follows the triggers set in the GitHub workflow.

The following secrets must be configured on GitHub:

  • SONARCLOUD_ORGANIZATION
  • SONARCLOUD_TOKEN

This also follows the properties defined inside the sonar-project.properties file, overwriting duplicates.

Deploy

Automatic build and deploy. Follows the triggers set in the GitHub workflow.

The following secrets must be configured on GitHub:

  • AZURE_SUBSCRIPTION_ID
  • AZURE_CLIENT_ID
  • AZURE_TENANT_ID
  • AZURE_FUNCTIONAPP_PUBLISH_PROFILE
Health Check

A health check will be made after each deploy and can also be manually triggered.

The following secrets must be configured on GitHub:

  • HEALTH_CHECK_URL
  • HEALTH_CHECK_METHOD

For health checking, simply call a get or a hit endpoint of your selected namespace/key pair. In this way the HEALTH_CHECK_URL must include the authentication code and the HEALTH_CHECK_METHOD would be GET.

License

This project is licensed under the MIT license. Feel free to edit and distribute this template as you like.

See LICENSE for more information.

Acknowledgements

The idea for this proof of concept emerged after using the free counting service CountAPI on some internal integration tests. We are using the same URL structure!

SonarCloud

About

A simple serverless counting API with Azure functions in Typescript

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published