Web Services in EK9

This is the final section on constructs, the sections after this are related to packaging and the compiler command line arguments. Web Services are deployed in conjunction with; and by Applications and Programs as shown in the introduction and structure sections.

These web services can be used for just rendering static/dynamic web sites or they can be used for REST microservices. The HTTPServer class is used in both cases as is the use of the service construct.

In its simplest form; it is possible to serve just pure file content. By using text constructs a simple templating solution (similar to velocity templates) can be implemented.Web services in EK9 are also aimed at providing full suport for REST.
But there is nothing to prevent developer creating full web UI tool kits.

The examples in this section show:

Clearly it is possible to move beyond CRUD implementations and use HATEOAS with HAL/ATOM or a bespoke link content solution.

But as the focus of this documentation is just to show the mechanisms and API's built into EK9 a simple CRUD application will suffice. This final example does deal with caching, etags and concurrency. So while the first two examples are short; the last example is quite long. It highlights different language mechanisms (composition and a blend of dynamic functions and classes) that can be used in EK9.

There is also a section interaction with the developed web service, which is also quite long. This aims to highlight the general value of caching, pre-condition checks and general CRUD (RPC) type web service interactions. Clearly to fully embrace REST further example of HATEOAS and links would be needed. These are really an aside from the EK9 language and so are not currently described.

Immutable software versus mutable configuration

You might argue that serving 'in memory' static content from an application is not really viable for anything in production; databases and various other 'stores' should be used to store this configuration data in isolation.
Wide spread use of 'docker' and automated CI/CD development cycles; means the speed and control of deploying fully tested and version controlled microservices is just as easy as it is to update configuration.

As the move towards 'immutable' infrastructure has progressed, applications have also become 'immutable'. This now means that unchanging configuration data and fixed information can be bound into an application. This one of the main reasons EK9 has the text construct. It is designed to facilitate the notional separation of data from code; whilst still allowing it to be bound into a version controlled release of an application. If you accept and value the use of 'caching'; then you already have 'immutable' configuration data (at least for the period of a cache lifetime).

If you prefer the older approach of putting everything in a database of some sort; then you can continue to do that with EK9. But it could be argued that only truly mutable data should be stored that way. Clearly the CRUD example shown later should store the data in some sort of resilient data store and not use a simple in memory model. But the focus of these examples is Web Services not resilient data storage.

It really comes down to the confidence in automated testing, the speed of builds and the rapidity and automation in deploying new software. In general it probably boils down to 'fear' and 'blame' if we're being honest. Some see updates to configuration data as some how less risky than deploying a new version of software.

Verbs and HTTP Headers

EK9 web services really focuses on caching support and for operations like PUT, PATCH and DELETE. EK9 enforces stale content checking and promotes the use of etag in preference to last modified header use.

If any of these terms are unfamiliar to you, please read up or refresh your knowledge of HTTP protocols and REST web services in general as this document assumes prior knowledge.

The Examples

The examples below only focus on the service construct the application and program have been covered elsewhere.

Static HTML content

The following example shows the text construct being used to return some simple HTML content. The important part here is the fact that the content is served from site/index.html via the GET verb.

The other important areas to focus on are the setting of the etag, status, content, contentType and finally but importantly the cacheControl.

#!ek9
defines module introduction

  defines text for "en"
    WebSite
      indexETag()
        "ABC123"
      index()
        `<html>
<head>
    <meta charset="UTF-8"/>
</head>
<body>
  <p>Hello, World</p>
</body>
</html>`

  defines service

    Site :/site

      index() as GET for :/index.html
        <- response as HTTPResponse

        webSite <- WebSite("en")
        response: (webSite) with trait of HTTPResponse

          override etag()
            <- rtn as String: webSite.indexETag()
          override cacheControl()
            <- rtn as String: "public,max-age=3600,must-revalidate"
          override contentType()
            <- rtn as String: "text/html"
          override contentLanguage()
            <- rtn as String: "en"
          override content()
            <- rtn as String: webSite.index()
          override status()
            <- rtn as Integer: 200
...
What's being shown

Even when dealing with just a simple HTML page; it is really important to focus on HTTP technology and techniques. There is a lot of capability in the HTTP protocol. The EK9 service construct has been created with the singular purpose of being the place to put all that code. It is shown 'long hand' above.

This means the service construct is the place to coordinate dealing with content negotiation, 'varies', 'mime types' and data formats. But most importantly the response should always be a 'dynamic' class that has the trait of HTTPResponse. This is really important as it is possible with EK9 to avoid doing any real hard processing to get content if you use the etag or lastModified methods correctly. If you also set the cacheControl then it is possible your code won't even get called (once supplied the first time)!

The EK9 HTTP Server deals with the remote client (or hopefully the intermediate proxy you put in front of the EK9 server); it calls on your response and firstly it only gets the etag or lastModified, then it checks if the calling system has passed any headers through. If the headers are present then the EK9 HTTP server may response with a 'not modified', if this is the case then the call to content() is never made. This is the most expensive call as it will trigger your business processing (which could access many Objects, Databases or other services).

In the example above, the code is trivial; it just gets content from a text Object in memory. But by providing the additional cacheControl directives; a caching proxy like squid would not even make any call at all to your service if the cached content was still in date. But better than that even when it is out of date, the squid proxy would use the etag as part of a request header. The EK9 server will then just response with a 'not modified' if your code returns the same etag value.

Having a quick and 'cheap' way for getting a resources etag or lastModified value is important.
It's hard to overstate how good the HTTP protocol is, it's also quite common to see it severely under utilised.

Software development versus solution deployment

These next few paragraphs are an aside from EK9 and Web Services and more a general observation (which you may or may not find you agree with).

There was a time when a software developer would understand the full end to end processing of an application. This would also include the physical hardware, the networking and even where the database (if used) was installed on the disks (the speed of the disks, etc). But as each area has come more specialised, software development has become fragmented (front-end, back-end, DBA, DBD, network engineer, security specialist). Then drawing all that together is your Enterprise Architect! These statements are not intended to be offensive (and your experiences may be very different). But hopefully you can see this point being made.

In short; as a pure software developer (coder) you may not even know about caching proxies like squid. Then when there is a performance problem with 'your' application; you decide it needs more hardware, CPU's or the database needs more memory etc. Maybe it even needs re-designing! Maybe the DBD needs to take a look at the database design and 'do some magic'.

The Experts

You'll probably find (if you work for an organisation - rather that just starting out developing software); there are a number of 'Characters', there is some sort of 'pecking order' or companies have specific histories with specific technologies or vendors. In short techie politics or just plain office politics. What I'm saying is that most organisations are truly dysfunctional, have distorted views, are technologically out of balance and are mismanaged.

But you still have to develop software in that environment (if you want to be paid) - but lets just be honest that's what it is like.

Why is this being discussed here and now! It's because we've left the safe realm of just coding (where we can argue about indentation and the merits of 'for loops'). We're now in the realm of where that coded software fits in a wider solution. It is now starting to show its characteristics for function/performance and reliability. It is no longer just visible to the software developers that created it (with the appropriate indentation after much debate).

If you are an expert with a hammer (all things look like nails) - lets pick on DBD's - 'your database needs views layering on it'!

Now lets pick on Unix/Linux/Windows Guru - 'you need OS version X with hyper something or other'.

Or maybe the Java/Spring master - 'Ah Spring boot will solve your problems'.

Lets not even start on Docker and Kubernetes guys, they'll build you a home crafted lumpy 'EJB' container. (Maybe that's being a bit harsh).

Finally the front end guys - we need to reinvent the UI framework wheel again!

The point being made here; is that each one of those experts may or may not have point. What they offer really may be of value (or it may not). Now the politics - it's very hard to get people to be objective about the issues faced and accept their solution might not be applicable. Depending on the force of character, the technological history or just poor management - you'll get certain solutions promoted/over what might be the most appropriate one. Everyone wants to reinvent the wheel (a bit rich from someone developing a new language; I know!).

See this article about the schisms inside organisations.

But in general the most robust/reliable/cost effective software is software that does not get written!

That's probably not what you want to hear; as the developer of a new language (EK9) it's not what I want to say! But I have to be honest - even if it means not writing any new code in a new wonderful language (EK9).

Anyway back to something techie - let's forget all that politics stuff and bury our heads back in the sand (interesting technology) - see be honest - that's what you and I both wanted at this point.

Networking/Caching

If you can get some sort of fast cache to supply the content your code provides that is cheaper; supports a higher volume of concurrent calls and is reasonably current, then do just that. There are times when you can't, but when you can; do.

If you focus on caching and can accept there is always some time when the user is looking at stale information then you can take the load of your application with something like nginx or squid for example. Take care with your responses and 'varies'. However, if and when you really depend on caching to save load there are some downsides.

If your systems are restarted for some reason and your caches are 'cold', all requests will be directed to your origin server (i.e. your code). So suddenly your code and the machines running it gets the full force of what that cache has been saving you from. BANG! most likely the machine will fall over.

So when you are at the extremes of loading and are using caching to really help, when you restart systems you must have operational procedures that ensure that the initial load is throttled in some way. This then enables all your caches to warm up. The alternative is to keep your system off line to the full load that is going to be applied; simulate calls through via your own scripts, this then warms all the caches.

The main point I'm attempting to make here is that 'devops' does actually mean something. If you are developing code and are also operating that code, you need to move out of your comfort zone and embrace 'operations', 'infrastructure', 'networking' and appliances (like caching software).

The paragraphs above are why the web service examples and the HTTPResponse look quite verbose. This is by design, EK9 does not try an hide the HTTP protocol away, it draws it right out so you can master it and provide sophisticated and performant solutions. The examples could have been written like more little 'Hello, World' examples, so as to claim I can do a web service in EK9 is 10 lines of code. But this rather misses the point of the examples and why and how web services do add quite a lot of functionality but also complexity. Just look at the number of variation of HTTP headers and response codes and the different 'verbs'.

A mini Web Server

The following example shows a really cut down web server, this application just serves text file content.

#!ek9
defines module introduction
  defines service
    WebServer :/website

      documentRoot()
        <- rtn as FileSystemPath: FileSystemPath(EnvVars().get("DOCUMENT_ROOT"))
        if not rtn?
          throw Exception("Invalid Document Root")

      file() as GET for :/{fileName}
        -> fileName as String
        <- response as HTTPResponse

        webServer <- this
        response: (webServer, fileName) with trait of HTTPResponse
          lastModified as DateTime: DateTime()

          private lastModifiedOfTextFile()
            <- rtn as DateTime: DateTime()
            file <- textFile()
            if not file.isReadable()
              Stdout().println(`${file} is not readable`)
            if not file.isFile()
              Stdout().println(`${file} is not a file`)
            rtn :=: file.lastModified()

          private textFile()
            <- rtn as TextFile: TextFile(webServer.documentRoot() + FileSystemPath(fileName))

          override lastModified()
            <- rtn as DateTime: DateTime()
            lastModified :=? lastModifiedOfTextFile()
            rtn :=: lastModified

          override cacheControl()
            <- rtn as String: "public,max-age=3600,must-revalidate"
          override contentType()
            <- rtn as String: "text/html"
          override contentLanguage()
            <- rtn as String: "en"
          override status()
            <- rtn as Integer: lastModified? <- 200:404

          override content()
            <- rtn as String: String()
            if lastModified?
              cat textFile() > rtn
...

There are a some points of interest in this example.

You may be wondering why do all this processing inside a dynamic class that has a trait of HTTPResponse. There are a couple of reasons for this:

A CRUD type Server

This next example is much longer, it is based around a repository for postal Addresses. The definition of the Address record and its marshalling to and from JSON is also included (currently EK9 does not have native support built-in for JSON).

Etags and HTTP verbs: POST (C), GET (R), PUT (U), DELETE (D) and PATCH (merge) are covered. But attention is also paid to concurrency issues through the use of a mutex lock. This has been done through an example that shows how to take an unsafe (in terms of concurrency) collection and make it safe for multiple threads to access.

The approach of this code example has been to show a blend of functional and object oriented approaches with a focus on composition. The composition approach has been extensively using in creating the HTTPResponse.

Some web service 'methods' have been left as 'long hand' so that it is more directly obvious what the processing is. But where the processing is so similar additional classes and functions have been composed in different ways to deliver the functionality.

The HTTP response codes '404' etc have not been hidden, nor have they been abstracted to constants. They have been left as is. So '404' could have been abstracted to a constant of NOT_FOUND (but this adds little other than code length for this example).

The internal model

The first part (and bulk) of the code just sets up the data structures and constructs needed for the example. The second part will focus on just the web services aspects.

#!ek9
defines module introduction

  defines type
    //Just for strong typing - no constraints
    AddressId is String

  defines record
    Address
      id as AddressId: AddressId()
      street as String: String()
      street2 as String: String()
      city as String: String()
      state as String: String()
      zipcode as String: String()

      Address()
        -> from as Address
        this :=: from

      Address()
        -> from as Optional of Address
        if from?
          this :=: from.get()          
      
      //Copy
      operator :=:
        -> from as Address
        id :=: from.id
        street :=: from.street
        street2 :=: from.street2
        city :=: from.city
        state :=: from.state
        zipcode :=: from.zipcode

      //Merge only if incoming address parts are set
      operator :~:
        -> address as Address        
        if address.street?
          street :=: address.street
        if address.street2?
          street2 :=: address.street2
        if address.city?
          city :=: address.city
        if address.state?
          state :=: address.state  
        if address.zipcode?        
          zipcode :=: address.zipcode
        
      operator ?
        //street2 is optional and can be omitted
        <- rtn as Boolean: id? and street? and city? and state? and zipcode?                

  defines text for "en"
    AddressToOutputFormat
      
      toJSON()
        -> address as Address
        `{
"address":
    {
    "id": "${address.id}",
    "street": "${address.street}",
    ${optionalJSON("street2", address.street2)}
    "city": "${address.city}",
    "state": "${address.state}",
    "zipcode": "${address.zipcode}"
    }
}`
  
  defines function

    //Used for specific operations to be applied to a set of addresses
    addressOperation() as abstract
      ->
        addresses as AddressAccess
        address as Address          

    //deals with wrapping the operation in the calls to deal with mutex lock.
    safeOperation()
      ->
        lockedAddressSet as MutexLock of AddressSet
        address as Address
        operation as addressOperation

      accessKey <- (address, lockedAddressSet, operation) of MutexKey
        override access()
          operation(lockedAddressSet.get(), address)
      lockedAddressSet.enter(accessKey)
                      
    addressFromJson()
      -> addressInJSONFormat as String
      <- rtn as Address: Address()

      addressParts <- addressDictionaryFromJSON(addressInJSONFormat)
      rtn.id: AddressId(addressParts.get("id"))
      rtn.street: String(addressParts.get("street"))
      rtn.street2: String(addressParts.get("street2"))
      rtn.city: String(addressParts.get("city"))
      rtn.state: String(addressParts.get("state"))
      rtn.zipcode: String(addressParts.get("zipcode"))

    <?-
      Until we have a more general JSON parser
      a bespoke parser is needed for this demonstration
    -?>
    addressDictionaryFromJSON()
      -> json as String
      <- rtn as Dict of (String, String): Dict()
      stdout <- Stdout()
      //Just pull out the address bits from within {}'s using a regex
      extractAddressPartsEx <- /\{\s+"address":\s+\{\s+([^}]*?)\s+\}\s+\}/
      //Now break into lines based on commas
      addressItems <- json.group(extractAddressPartsEx).first().split(/,/)
      //Map to a dictionary and return.
      rtn: cat addressItems | map with toDictEntry | collect as Dict of (String, String)      

    toDictEntry()
      -> line as String
      <- rtn as DictEntry of (String, String): DictEntry()
      keyValues <- line.trim().split(/:/)
      rtn: DictEntry(keyValues.first().trim().trim('"'), keyValues.last().trim().trim('"'))

    copyAddress()
      -> from as Address
      <- to as Address: Address(from)
    
    commaSeparated()
      ->
        firstPart String
        secondPart String
      <-
        rtn as String: firstPart? and secondPart? <- firstPart + "," + secondPart : String()

    addressToJSON()
      -> address as Address
      <- addressAsString as String: AddressToOutputFormat("en").toJSON(address)

    addressListToJSON()
      -> addresses as List of Address
      <- listAsString as String

      body <- cat addresses | map with addressToJSON | join with commaSeparated | collect as String
      listAsString := `[ ${body} ]`

    optionalJSON()
      ->
        name as String
        value as String
      <-
        rtn as String: value? <- `"${name}": "${value}",` else String()

  defines trait
    AddressAccess
      operator +=
        -> address as Address
        <- rtn as AddressAccess
        
      operator -=
        -> address as Address
        <- rtn as AddressAccess

      //merge with an existing address
      operator :~:
        -> address as Address
        <- rtn as AddressAccess

      //replace an existing address
      operator :^:
        -> address as Address
        <- rtn as AddressAccess
        
      operator contains
        -> addressId as AddressId
        <- rtn as Boolean
          
      hash()
        <- rtn as String
        
      hashOfAddress()
        -> id as AddressId
        <- rtn as String
                
      byId()
        -> id as AddressId
        <- rtn as Address
        
      listAll()
        <- rtn as List of Address
                                       
  defines class
    AddressSet with trait of AddressAccess
      hash as String: HMAC().SHA256(GUID())
      addresses as Dict of (AddressId, Address): Dict()
      hashes as Dict of (AddressId, String): Dict()

      override operator +=
        -> address as Address
        <- rtn as AddressAccess: this
        assert address?
        
        if this not contains address
          includeAddress(address)
      
      override operator -=
        -> address as Address
        <- rtn as AddressAccess: this
        assert address.id?
        
        addresses -= address.id
        hashes -= address.id
        updateHash()      

      override operator :~:
        -> address as Address
        <- rtn as AddressAccess: this
        assert address.id?
        //We don't assert whole address because it can be partial

        currentAddress <- addresses.get(address.id)
        if currentAddress?
          //make a new copy and then merge the two.
          updatedAddress <- Address(currentAddress)
          updatedAddress :~: address
          includeAddress(updatedAddress)

      override operator :^:
        -> address as Address
        <- rtn as AddressAccess: this
        assert address?
                        
        if this contains address
          includeAddress(address)
      
      operator contains
        -> address as Address
        <- rtn as Boolean: this contains address.id      
    
      override operator contains
        -> addressId as AddressId
        <- rtn as Boolean: addresses contains addressId      
      
      override hash()
        <- rtn as String: this.hash

      override hashOfAddress()
        -> id as AddressId
        <- rtn as String: String(hashes.get(id))

      override byId()
        -> id as AddressId
        <- rtn as Address: Address(addresses.get(id))
      
      override listAll()
        <- rtn as List of Address: List()

        iter <- addresses.values()
        cat iter | map with copyAddress > rtn

      private includeAddress()
        -> address as Address
                
        copy <- Address(address)
        addresses += DictEntry(copy.id, copy)
        hashes += DictEntry(copy.id, HMAC().SHA256(addressToJSON(copy)))
        updateHash()

      private updateHash()
        hash :=: HMAC().SHA256(GUID())

    //Example of wrapping shared data set in a mutex lock.
    LockableAddressSet with trait of AddressAccess
      lockedAddressSet as MutexLock of AddressSet: MutexLock(AddressSet()) 
      
      override operator +=
        -> address as Address
        <- rtn as AddressAccess: this
        
        //You can inline this simple dynamic function if you wish
        //Also used named parameters
        safeOperation(
          lockedAddressSet: lockedAddressSet,
          address: address,
          operation: () is addressOperation (addresses += address)
          )
                  
      override operator -=
        -> address as Address
        <- rtn as AddressAccess: this

        //Or you can inline all on one line.
        safeOperation(lockedAddressSet, address, () is addressOperation (addresses -= address))

      override operator :~:
        -> address as Address
        <- rtn as AddressAccess: this

        //Or define a dynamic function and pass in as delegate (my preferred way)
        operation <- () is addressOperation
          addresses :~: address
        safeOperation(lockedAddressSet, address, operation)        

      override operator :^:
        -> address as Address
        <- rtn as AddressAccess: this
        operation <- () is addressOperation
          addresses :^: address
        safeOperation(lockedAddressSet, address, operation)

      override operator contains
        -> addressId as AddressId
        <- rtn as Boolean: false
        accessKey <- (addressId, lockedAddressSet, rtn) of MutexKey
          override access()
            rtn :=: lockedAddressSet.get() contains addressId          
        lockedAddressSet.enter(accessKey)
        
      override hash()
        <- rtn as String: String()
        accessKey <- (rtn, lockedAddressSet) of MutexKey
          override access()
            rtn :=: lockedAddressSet.get().hash()            
        lockedAddressSet.enter(accessKey)

      override hashOfAddress()
        -> id as AddressId
        <- rtn as String: String()
        accessKey <- (id, rtn, lockedAddressSet) of MutexKey
          override access()
            rtn :=: lockedAddressSet.get().hashOfAddress(id)
        lockedAddressSet.enter(accessKey)

      override byId()
        -> id as AddressId
        <- rtn as Address: Address()
        accessKey <- (id, rtn, lockedAddressSet) of MutexKey
          override access()
            rtn :=: lockedAddressSet.get().byId(id)
        lockedAddressSet.enter(accessKey)

      override listAll()
        <- rtn as List of Address: List()
        accessKey <- (rtn, lockedAddressSet) of MutexKey
          override access()
            rtn += lockedAddressSet.get().listAll()
        lockedAddressSet.enter(accessKey)

  defines function
    plainNonCacheableHTTPResponse()
      <- rtn as HTTPResponse
      rtn: () of HTTPResponse
        override cacheControl()
          <- rtn as String: "no-store,max-age=0"
        override contentType()
          <- rtn as String: "text/plain"
        override contentLanguage()
          <- rtn as String: "en"

    cacheableHTTPResponse()
      <- rtn as HTTPResponse
      rtn: () of HTTPResponse
        override cacheControl()
          <- rtn as String: "public,max-age=5,must-revalidate"
        override contentType()
          <- rtn as String: "application/json"
        override contentLanguage()
          <- rtn as String: "en"

  defines class

    ByETagHTTPResponse with trait of HTTPResponse by delegate
      repository as Repository!

      delegate as HTTPResponse?
      addressId as AddressId?
      provideContentLocation as Boolean?

      status as Integer: 200
      etagOfAddress as String: String()
      
      private ByETagHTTPResponse()
        assert false
        
      ByETagHTTPResponse()
        ->
          addressId as AddressId
          delegate as HTTPResponse
          provideContentLocation as Boolean
        assert addressId? and delegate? and provideContentLocation?

        this.addressId: addressId
        this.provideContentLocation: provideContentLocation
        this.delegate: delegate

      ByETagHTTPResponse()
        ->
          addressId as AddressId
          delegate as HTTPResponse
        this(addressId, delegate, true)
        
      override etag()
        <- rtn as String: String()
        //Only call if un-set.
        etagOfAddress :=? repository.addresses().hashOfAddress(addressId)            
        rtn :=: etagOfAddress
        if ~etagOfAddress?
          status: 404

      override status()
        -> newStatus as Integer
        status :=: newStatus
      override status()
        <- rtn as Integer: status
      override contentLocation()
        <- rtn as String: status < 400 and provideContentLocation <- `/addresses/${addressId}` else String()
    
...

The code above shows a mix of EK9 constructs, these are used to hold a set of Addresses ()record) in memory, but limit access via a mutex lock. The use of dynamic functions as delegates has been employed for this.

By extracting AddressAccess out to a trait construct two implementations can be defined. The first is the actual address storage and the second is just a simple thread safe wrapper.

The next and final part covers the service definition. This shows the use of the caching HTTPResponse implementations.

...
  <?-
    REST web service.    
  -?>
  defines service
    //The Name of the and the uri it is mapped to
    Addresses :/addresses

      byId() as GET for :/{address-id}
        -> addressId as AddressId :=: PATH "address-id" //required because different name
        <- response as HTTPResponse

        delegate <- ByETagHTTPResponse(addressId, cacheableHTTPResponse(), false)
        response: (addressId, delegate) of HTTPResponse by delegate
          repository as Repository!

          override content()
            <- rtn as String: String()
            if delegate.status() <> 404
              rtn: addressToJSON(repository.addresses().byId(addressId))
              status(200)

      //A POST
      operator += :/
        -> request as HTTPRequest :=: REQUEST          
        <- response as HTTPResponse
        
        nonCacheable <- plainNonCacheableHTTPResponse()
        
        response: (request, nonCacheable) of HTTPResponse by nonCacheable
          repository as Repository!
          address as Address: Address()
          status as Integer: 201

          override content()
            <- rtn as String: String()            
            address: addressFromJson(request.content())
            //But the server sets the ID!
            if address.id?
              status := 422 //unprocessable entity
              rtn: "Do not supply ID in Address, server will set this"
            else
              address.id: AddressId(GUID())
              if ~address?
                status := 422 //unprocessable entity
              else if repository.addresses() contains address.id
                status := 409 //conflict
              else            
                repository.addresses() += address
          
          override contentLocation()
            <- rtn as String: status == 201 <- `/addresses/${address.id}` else String()
          override status()
            <- rtn as Integer: status  
            
      //A DELETE
      operator -= :/{addressId}
        -> addressId as AddressId //Assume PATH
        <- response as HTTPResponse
                
        delegate <- ByETagHTTPResponse(addressId, plainNonCacheableHTTPResponse(), false)
        response: (addressId, delegate) of HTTPResponse by delegate
          repository as Repository!
                    
          override content()
            <- rtn as String: String()
            //Only if the etag was found can we delete it!
            if delegate.status() <> 404
              repository.addresses() -= repository.addresses().byId(addressId)
              status(204)

      //A PATCH which is a merge
      operator :~: :/{addressId}
        ->
          addressId as AddressId //Assume PATH
          incomingContent as String :=: CONTENT          
        <-
          response as HTTPResponse
        
        delegate <- ByETagHTTPResponse(addressId, plainNonCacheableHTTPResponse())
        
        response: (addressId, incomingContent, delegate) of HTTPResponse by delegate
          repository as Repository!

          override content()
            <- rtn as String: String()
            if delegate.status() <> 404
              address <- addressFromJson(incomingContent)
              if ~address.id?
                status(422) //unprocessable entity
              else if address.id <> addressId
                status(400) //the id on the url is not the same as the id in the body content
              else
                repository.addresses() :~: address
                status(204)

      //A PUT which is a replace for an existing address
      operator :^: :/{addressId}
        ->
          addressId as AddressId //Assume PATH
          incomingContent as String :=: CONTENT          
        <-
          response as HTTPResponse
        
        delegate <- ByETagHTTPResponse(addressId, plainNonCacheableHTTPResponse())

        response: (addressId, incomingContent, delegate) of HTTPResponse by delegate
          repository as Repository!
 
          override content()
            <- rtn as String: String()
            if delegate.status() <> 404
              address <- addressFromJson(incomingContent)
              if ~address?
                status(422) //unprocessable entity
              else if address.id <> addressId
                status(400) //the id on the url is not the same as the value in the body content
              else
                repository.addresses() :^: address
                status(204)

      listAll() :/
        <- response as HTTPResponse

        cacheable <- cacheableHTTPResponse()
        response: (cacheable) of HTTPResponse by cacheable
          repository as Repository!
          
          override etag()
            <- rtn as String: repository.addresses().hash()
          override content()
            <- rtn as String: addressListToJSON(repository.addresses().listAll())

  defines component
    Repository as abstract
     
      addresses() as abstract
        <- rtn as AddressAccess     
      
    InMemoryRepository extends Repository
      addresses as AddressAccess: LockableAddressSet()
            
      InMemoryRepository()
        addresses += Address(AddressId(GUID()), "121 Admin Rd.", String(), "Concord", "NH", "03301")
        addresses += Address(AddressId(GUID()), "67 Paperwork Ave", String(), "Manchester", "NH", "03101")
        addresses += Address(AddressId(GUID()), "15 Rose St", "Apt. B-1", "Concord", "NH", "03301")
        addresses += Address(AddressId(GUID()), "39 Sole St.", String(), "Concord", "NH", "03301")
        addresses += Address(AddressId(GUID()), "99 Mountain Rd.", String(), "Concord", "NH", "03301")     
                       
      override addresses()
        <- rtn as AddressAccess: addresses       

  defines application
    AccessPoint
      //We could register other services and components here
      
      register InMemoryRepository() as Repository
      register Addresses()
    
//EOF

Summary

The HTTP Server built into EK9 is not designed to be the highest performance, the most configurable nor the most flexible. It is designed to get out of the way. This should enable you as the developer to focus on the services you offer, the data formats needed and caching support.

If you want high performance, use caching (i.e. hardly call the origin server at all). Even with dynamic data and short cache life times; this can be done with must-revalidate. Use multiple instances via docker and Kubernetes for example.

You cannot really do web services without focusing on caching, HTTP response codes and concurrent access to data.

Next Steps

The details on packaging and deploying/publishing your code to an artefact server are covered in the next section on packaging.

But if you are looking for more details on the command line parameters see the command line section.