Serverless Racket Applications Using Azure Functions Custom Handlers
Introduction
Racket is a fun and powerful general purpose programming language based on the Scheme dialect of Lisp that provides tools and packages that allow individuals to quickly be productive. Although you can build traditional web applications with it, it would be nice to use it with cloud-native technologies like serverless. Azure Functions is designed for these types of event-driven workflows but unfortunately does not officially support Racket. Recently, a new feature called custom handlers was announced which allows individuals to run web applications written in any language that supports HTTP primitives as an Azure Function. When I learned of this feature, my immediate thought was, challenge accepted!
Custom handlers require the following:
- Write a web server to process requests
- Define the bindings for the request and response function payloads
- Configure the Azure Functions host to send request to the web server
In this writeup, I'll show how to set up a Racket web server that processes GET
requests running as an Azure Function. The source code for this project can be found in the RacketAzureFunctionsCustomHandlerSample GitHub repository
Prerequisites
This project was built on a Windows 10 PC, but it should work cross-platform on Mac and Linux.
- Node.js
- Racket
- Azure Functions Core Tools. This sample uses v2.x of the tool.
Create Azure Functions project
Create a new directory called RacketAzureFunctionsCustomHandlerSample
and navigate to it.
Create Racket server
The way Azure Functions custom handlers work is by having the Azure Functions host proxy requests to a web server written in the language of choice which processes the request and sends the response back to the Azure Functions host.
Start by setting the environment variable where Azure Functions and the server listen on. Create and environment variable called FUNCTIONS_HTTPWORKER_PORT
. In this example, I set the variable to 7071
.
Inside of your application directory, create a file called server.rkt which will contain the server logic.
Open the server.rkt file. Define the language and import the required packages.
#lang racket
(require json)
(require web-server/servlet)
(require web-server/servlet-env)
Then, get the port where the server listens on from the FUNCTIONS_HTTPWORKER_PORT
environment variable.
(define PORT (string->number (getenv "FUNCTIONS_HTTPWORKER_PORT")))
Next, create a function called get-values
to process your request. In this case, the function receives a GET
request that returns a JSON object containing a list of integers.
(define (get-values req)
(response/full
200
#"OK"
(current-seconds)
#"application/json;charset=utf-8"
empty
(list (jsexpr->bytes #hasheq((value . (1 2 3)))))))
After that, define the routes so your server dispatches requests to the appropriate endpoint. In this case, GET
requests to the /values
endpoint are sent to and processed by the get-values
function.
(define-values (dispatch req)
(dispatch-rules
[("values") #:method "get" get-values]
[else (error "Route does not exist")]))
The final server.rkt file should contains content similar to the one below:
;; Define language and import packages
#lang racket
(require json)
(require web-server/servlet)
(require web-server/servlet-env)
;; Get port where server listens on
(define PORT (string->number (getenv "FUNCTIONS_HTTPWORKER_PORT")))
;; Create function to handle GET /values request
(define (get-values req)
(response/full
200
#"OK"
(current-seconds)
#"application/json;charset=utf-8"
empty
(list (jsexpr->bytes #hasheq((value . (1 2 3)))))))
;; Define routes
(define-values (dispatch req)
(dispatch-rules
[("values") #:method "get" get-values]
[else (error "Route does not exist")]))
;; Define and start server
(serve/servlet
(lambda (req) (dispatch req))
#:launch-browser? #f
#:quit? #f
#:port PORT
#:servlet-path "/"
#:servlet-regexp #rx"")
Test the Racket server
Start the server by running the following command:
racket --require server.rkt
Then, using an application like Postman or Insomina, make a GET
request to http://localhost:7071/values
.
The response should look like the following:
{
"value": [
1,
2,
3
]
}
Define function bindings
The way Azure Functions discovers functions is through subdirectories containing a binding definition called function.json. The name of the subdirectories must match the name of your function's route path. For example if the route path is /values
, then the name of the subdirectory is values
.
Create a subdirectory inside the main application directory called values
Inside the values subdirectory, create a file called function.json and add the following content to it.
{
"bindings": [
{
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": ["get"]
},
{
"type": "http",
"direction": "out",
"name": "res"
}
]
}
The function.json file defines the request and response payloads. In this case, the incoming request is an HttpTrigger
that only handles GET
requests and returns an HTTP response.
Create server executable
Package your server application into a single executable by entering the following command into the command prompt:
raco exe server.rkt
Once your application is packaged, an executable with the name server.exe should be created in your application directory.
Configure Azure Functions host
In your application directory, create a file called host.json and add the following contents:
{
"version": "2.0",
"httpWorker": {
"description": {
"defaultExecutablePath": "server.exe"
}
}
}
This host.json configuration file tells the Azure Functions host where to find the web server executable.
Run the Azure Functions application
Inside the root application directory, enter the following command into the command prompt.
func start
Using an application like Postman or Insomnia, make a GET
request to localhost:7071/api/values
.
The response should look like the following:
{
"value": [
1,
2,
3
]
}
Conclusion
In this writeup, I showed how to create a Racket serverless application that runs on Azure Functions by using custom handlers. Doing so requires you to:
- Write a web server to process requests
- Define the bindings for the request and response function payloads
- Configure the Azure Functions host to send request to the web server
Although in this example, the server was written in Racket, the same process is applicable to other languages. Keep in mind that at the time of this writing, custom handlers are preview and may change. Happy coding!