A simple serverless counting API with Azure functions in Typescript.
- About the application
- Getting started
- Usage
- Development
- Continuous integration
- License
- Acknowledgements
A simple serverless counting API with Azure functions in Typescript. Proof of concept and template for greater APIs!
- The
get
andhit
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.
- 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
- 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
Environment configuration
- 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
Recommended VS Code extensions:
- SonarLint - Code linting
- ES Lint - JavaScript linting
- Pretty TypeScript Errors - Prettier and human-readable TypeScript errors
- Error Lens - Highlighting of errors and other language diagnostics
- REST Client - Lightweight Rest Client
- MySQL client - MySQL database client
- GitLens - Git extensions
- Prettier - Code formatter
- TypeScript Import Sorter - Sort imports and lowers merge conflicts
- Code Spell Checker - Spell checker
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
- Azure (example): https://counter.azurewebsites.net
- Local: http://localhost:7071
- {{Action}} =>
get
to get the current value orhit
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"
}
}
Running locally
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
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
Formatting
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
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
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 openPRs
and changes to themain
branch.
To run an analysis locally with SonarQube and Docker:
yarn sonar-server:start
OR
yarn ss
[Optional] Persist analysis results
To persist the analysis results when running a local server of SonarQube:
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!
yarn sonar-server:start-persistent
OR
yarn ssp
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
yarn sonar
OR
yarn s
Pre-commit hook
The
pre-commit
hook should run automatically before every commit throughHusky
.
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
.
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
.
This project is licensed under the MIT license. Feel free to edit and distribute this template as you like.
See LICENSE for more information.
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!