What are the rules for valid service URI paths and method mapping?

← Web Services · Ref: Q684

EK9 services map URI paths to methods and operators. Several rules govern valid service definitions, all checked at compile time to prevent runtime routing errors.

URI PATH SYNTAX

The base path is declared on the service name ('ProductService :/products'), and each method adds a relative path ('featured() as GET for :/featured'), both written with ':/'.
A path written without the colon ('for /featured') is not a URI path at all: it is a syntax error, reported before any semantic check.

NO DUPLICATE ENDPOINTS (E02070)

Two methods or operators cannot map to the same HTTP verb and path structure. Both clashing methods are reported with E02070 ('PATH is duplicated in terms of structure/naming').

PATH PARAMETER MATCHING (E07730, E07700)

Every '{name}' in the path needs a method parameter, and every PATH parameter needs a '{name}' in the path: 'findById() as GET for :/{productId}' takes '-> productId as String'. Leaving the parameter out of the method gives E07730 (PATH variable count and PATH parameter count mismatch). A parameter whose '{name}' is not in the path is E07700.

PATH PARAMETER TYPES (E07760)

A path parameter does not have to be a String. Integer, String, Date, Time, DateTime, Milliseconds and Duration are converted from the URI for you; anything else (Float, for example) is E07760.

See Q657 for URI mapping basics. See Q658 for path parameter binding. See Q659 for service return types.

Example

defines module qa.webdeep.uripaths

  defines constant

    JSON_CONTENT_TYPE <- "application/json"
    ENGLISH_LANGUAGE <- "en"

  defines service

    <?-
      Service demonstrating valid URI paths with distinct endpoints.
      Base path plus method-level paths, all unique.
    -?>
    ProductService :/products open

      //GET /products/featured — no path params
      featured() as GET for :/featured
        <- response as HTTPResponse: () with trait HTTPResponse
          override content()
            <- rtn as Bytes: Bytes().withUtf8(`["featured-item"]`)
          override status() as pure
            <- rtn as Integer: 200
          override contentType() as pure
            <- rtn as String: JSON_CONTENT_TYPE
          override cacheControl() as pure
            <- rtn as String: "max-age=120"
          override contentLanguage() as pure
            <- rtn as String: ENGLISH_LANGUAGE
          default operator ?

      //GET /products/{productId} — path param matches method param
      findById() as GET for :/{productId}
        -> productId as String
        <- response as HTTPResponse: (capturedId: productId) with trait HTTPResponse
          override content()
            <- rtn as Bytes: Bytes().withUtf8(`{"id": "${capturedId}"}`)
          override status() as pure
            <- rtn as Integer: 200
          override contentType() as pure
            <- rtn as String: JSON_CONTENT_TYPE
          override cacheControl() as pure
            <- rtn as String: "no-cache"
          override contentLanguage() as pure
            <- rtn as String: ENGLISH_LANGUAGE
          default operator ?

      //GET /products/{categoryName}/items — different path, no conflict
      byCategory() as GET for :/{categoryName}/items
        -> categoryName as String
        <- response as HTTPResponse: (capturedCategory: categoryName) with trait HTTPResponse
          override content()
            <- rtn as Bytes: Bytes().withUtf8(`{"category": "${capturedCategory}"}`)
          override status() as pure
            <- rtn as Integer: 200
          override contentType() as pure
            <- rtn as String: JSON_CONTENT_TYPE
          override cacheControl() as pure
            <- rtn as String: "max-age=60"
          override contentLanguage() as pure
            <- rtn as String: ENGLISH_LANGUAGE
          default operator ?

  defines application

    ProductApp
      register ProductService()

  defines program

    ServiceUriPathsDemo()
      stdout <- Stdout()
      stdout.println("Service URI paths:")
      stdout.println("  GET /products/featured")
      stdout.println("  GET /products/{productId}")
      stdout.println("  GET /products/{categoryName}/items")

Common mistakes

E50001 — The function getUriPaths is not defined in this module. See ek9 -h E50001 for details.

Incorrect:

stdout.println(getUriPaths())

Correct:

stdout.println("Service URI paths:")

E50001 — The function getFeaturedPath is not defined in this module. See ek9 -h E50001 for details.

Incorrect:

stdout.println(getFeaturedPath())

Correct:

stdout.println("  GET /products/featured")
Other ways to ask this
  • What happens if a service URI path is written without the leading colon?
  • What is E02070 duplicate service endpoint?
  • What is E07730 path variable and parameter count mismatch?
  • What types can a service path parameter have?

Coming from another language?

Java: @RequestMapping with @PathVariable annotations. Python: Flask @app.route('/path/<param>'). Go: gorilla/mux with {param} syntax. Rust: Actix web::resource().route(). EK9: service-level :/path syntax with compile-time validation of path-to-parameter binding.

Keywords: path, E07730, E02070, http, endpoint, dict, duplicate, URI, service, function, E07700, E07760, REST