AnnouncementsIntroducing Arkoren

    Menu

    Quickstart

    Extend the generated HTTP service with a greeting route, a process-local counter, and a checked error response.

    What's on this page
    1. Run the starter and confirm its welcome page and readiness probe.
    2. Connect an error-returning handler to a POST route.
    3. Add an atomic cache counter with application-owned startup and cleanup.
    4. Return a client-safe problem response and stop the service cleanly.
    On this page

    Build a greeting endpoint, count greetings in memory, and reject an oversized name with an HTTP error. No database, broker, or credentials are needed.

    Start with the hello service from Creating Projects, including its dependency-resolution step. Run every command below from that project’s root. Keep the generated bootstrap files: the edits extend them rather than replacing their lifecycle, middleware, or probes.

    First Request

    Start the service on loopback:

    HTTP_ADDR=127.0.0.1:8080 go run ./cmd/server

    The shell examples use POSIX environment assignments. In PowerShell, set $env:HTTP_ADDR = "127.0.0.1:8080" before running go run ./cmd/server. If port 8080 is occupied, choose another port and use it in the requests below.

    The development .env enables unauthenticated diagnostics on the same listener. Keep the loopback address; do not expose this development process publicly. Starter Defaults explains the settings.

    In another terminal, request the welcome page and readiness probe:

    curl -i http://127.0.0.1:8080/
    curl -i http://127.0.0.1:8080/readyz

    Both return 200 OK; / contains an HTML welcome page and /readyz contains ok. The server’s startup log reports its bound address. If binding fails, the process exits unsuccessfully rather than reporting a successful HTTP start.

    Stop with Ctrl+C before editing. Repeat the start command after each edit below; the starter does not watch or reload source files.

    Greeting Handler

    An Arkoren HTTP handler takes the familiar response writer and request, then returns an error. The router renders returned errors centrally, so handlers can return failures without writing a second response.

    Create internal/bootstrap/greeting.go. Start it with the package, imports, and a typed response:

    package bootstrap
    
    import (
    	"net/http"
    
    	"arkoren.dev/core/http/response"
    )
    
    // Greeting is the JSON returned for an accepted greeting.
    type Greeting struct {
    	Message string `json:"message"`
    }

    Append this handler to the same file. It reads a route wildcard named name and returns JSON:

    // greet responds to a greeting request using the route's name.
    func greet(w http.ResponseWriter, r *http.Request) error {
    	name := r.PathValue("name")
    
    	return response.JSON(w, http.StatusOK, Greeting{
    		Message: "Welcome, " + name + "!",
    	})
    }

    response.JSON sets the content type and encodes the response. Returning its error keeps encoding or write failures visible to the router.

    Route Registration

    In internal/bootstrap/router.go, inside the callback returned by Router(), add this line before return routes:

    routes.Post("/greetings/{name}", greet)

    Keep the existing middleware, welcome route, and probe routes. Registration maps the method and path to the handler; it does not open a listener. Use POST because the next step records a greeting and changes state.

    Restart the service, then send a greeting:

    curl -i -X POST http://127.0.0.1:8080/greetings/Mira

    The response is 200 OK with this JSON content:

    {
      "message": "Welcome, Mira!"
    }

    The starter’s CSRF protection allows this command-line request without Origin or Sec-Fetch-Site headers. That is not authentication or permission to expose state-changing routes without an application security policy.

    Counting Greetings

    Keep counters behind a cache facade so the handler does not depend on how values are stored. In internal/bootstrap/bootstrap.go, add these imports alongside the existing application import:

    "arkoren.dev/core/service/cache"
    "arkoren.dev/core/service/cache/memorycache"

    Inside Compose, replace only routes := compose.Use(Router()) with:

    driver := memorycache.New()
    compose.Cleanup(driver.Stop)
    compose.Startup(driver.Start)
    visits := cache.New(driver)
    
    routes := compose.Use(Router(visits))

    Keep the following compose.UseDebug(MCP(routes)) and compose.Use(Server(routes)) calls. Cleanup is registered immediately at the driver’s owner and runs in reverse order, so the server stops before the cache. Composition finishes before startup starts the cache’s expiry janitor and binds the HTTP listener. Failures during composition or startup also release acquired resources.

    In internal/bootstrap/router.go, import arkoren.dev/core/service/cache and change the factory declaration to accept the facade:

    func Router(visits *cache.Cache) application.Factory[*router.Router] {

    Keep the existing factory body and replace the greeting registration with this closure, which passes the shared facade to the handler:

    routes.Post("/greetings/{name}", func(w http.ResponseWriter, r *http.Request) error {
    	return greet(w, r, visits)
    })

    Add net/http to that file’s imports. In greeting.go, add the cache import and a Count int64 field with the tag json:"count" to Greeting. Replace the entire greet function with:

    // greet counts an accepted greeting and returns its current process-local total.
    func greet(w http.ResponseWriter, r *http.Request, visits *cache.Cache) error {
    	name := r.PathValue("name")
    	key := cache.NewKey[int64]("greetings." + name)
    	count, err := visits.Increment(r.Context(), key, 1)
    	if err != nil {
    		return err
    	}
    
    	return response.JSON(w, http.StatusOK, Greeting{
    		Message: "Welcome, " + name + "!",
    		Count:   count,
    	})
    }

    The typed key names Mira’s counter exactly greetings.Mira. Increment creates a missing counter and updates it atomically; it does not use the key’s write TTL. These counters have no expiry, belong only to this process, and reset on restart. They are demonstration state, not durable business records or an exactly-once record of delivered responses. A write can succeed even if sending its response later fails.

    Restart, then issue the POST request twice. The second response contains:

    {
      "message": "Welcome, Mira!",
      "count": 2
    }

    For shared counters across replicas, follow Cache Drivers for backend construction and ownership. The handler continues to use the facade. Use Key Definitions and Counters when you need reusable naming or bounded counter windows.

    Expected Errors

    Reject an oversized name before changing its counter. Add arkoren.dev/foundation/problem to greeting.go’s imports. Immediately after name := r.PathValue("name"), insert:

    if len(name) > 32 {
    	return problem.UnprocessableContent().Derive(func(details *problem.Builder) {
    		details.Detail("The name must be at most 32 bytes.")
    	})
    }

    len counts bytes, not Unicode characters. Restart and send 33 ASCII characters:

    curl -i -X POST http://127.0.0.1:8080/greetings/abcdefghijklmnopqrstuvwxyzabcdefg

    The response has status 422 Unprocessable Content and a Problem Details body:

    {
      "type": "about:blank",
      "title": "Unprocessable Content",
      "status": 422,
      "detail": "The name must be at most 32 bytes.",
      "instance": "/greetings/abcdefghijklmnopqrstuvwxyzabcdefg"
    }

    No counter is incremented for this rejected request. The router can render an error only before the response is committed; it cannot replace headers or body bytes already sent. See Error Handling for private internal failures and custom problem responses.

    Stopping Safely

    Press Ctrl+C in the service terminal. The generated executable’s signal context also handles SIGTERM. The application stops serving, runs registered cleanup, and returns before the executable exits; clean shutdown exits with status 0, while lifecycle failures exit with status 1.

    The HTTP runner allows in-flight requests up to 15 seconds to drain by default. After exit, another request fails to connect and the memory counters are gone. This walkthrough creates no persistent backend or container to remove. Keep or delete only the hello project you created; do not remove a shared parent directory.

    You now have a working route, a shared dependency inside one process, and a checked error path. Continue with Configuration to translate application settings, Project Layout to separate features, or Deployment before running outside development.

    Open a bulk documentation archive?

    This file contains every guide in its scope, not API declarations. Loading it all into an agent can exhaust its context window and increase processing time and cost. For coding tasks, start with the index and read only the relevant pages.

    Use the full archive for offline indexing, bulk processing, or deliberate large-context use.

    Browse the index instead
    Open archive