Skip to content
FutureBlitz

    Install

    Projects, 2 of 3

    An HTTP service (high scores)

    Build a high-score service with std.net.http, its routes a Class's Methods, JSON in and out, tried with HttpClient.

    What you’ll build

    A small high-score service: POST /scores adds a score, GET /scores lists the best, and GET /scores/:player gives one player’s best. It uses std.net.http’s HttpServer, whose routes are a Class’s Async Methods, and std.data’s Json. Each step’s program starts the server, sends it a few requests with HttpClient, prints the answers, and stops, so you can run it and see the whole exchange. The last step runs it for real.

    A route

    An Async Method with @Get(path) is a route. server.Routes(object) adds every route of the object’s Class. Listen on port 0 lets the OS pick a free port, and Serve() answers requests until Stop().

    Import std.core
    Import std.net
    Import std.net.http
    
    Class Hello
        @Get("/hello")
        Async Method Hello(): HttpResponse => HttpResponse.Of(Body.Text("hello from FutureBlitz"))
    End Class
    
    Local server = HttpServer()
    server.Routes(Hello())
    Try
        server.Listen(SocketAddress(IpAddress.Loopback, 0))
        Local serving = server.Serve()
    
        Local client = HttpClient()
        Local response = Await client.GetAsync("http://127.0.0.1:{server.LocalAddress.Port}/hello")
        Print(response.Status, response.Text)
    
        server.Stop()
        Await serving
    Catch e As Error
        PrintError("failed: {e.Message}")
    End Try
    Output
    200 hello from FutureBlitz

    Serve() gives a Task that runs while the program goes on. Await waits for the client’s response without blocking the thread, which the server’s work also runs on: the server’s handlers are Async code on the main thread, so they share the program’s data without data races.

    Scores in and out

    A route’s one argument is a Struct that the server fills from the request: a @JsonBody field from its JSON body, and the other fields from the path and the query, each read as its field’s type. A response’s JSON comes from a Struct or an array of them, through Json.Write.

    Import std.core
    Import std.collections
    Import std.data
    Import std.net
    Import std.net.http
    
    Struct Score
        Field Player: String
        Field Points: Int
    End Struct
    
    Struct NewScore
        @JsonBody Field Score: Score
    End Struct
    
    Struct Top
        Field Top: Int = 3
    End Struct
    
    Class Scores
        Field Board: Score[] = []
    
        @Post("/scores")
        Async Method Add(args: NewScore): HttpResponse
            Board.Add(args.Score)
            Board.SortByDescending(Function(s: Score) => s.Points)
            Return HttpResponse.Of(StatusCode.CREATED, Body.Text("added"))
        End Method
    
        @Get("/scores")
        Async Method Best(args: Top): HttpResponse
            Local best = Board.View(0, Min(args.Top, Board.Count)).ToArray()
            Return HttpResponse.Of(Body.Json(Json.Write(best)))
        End Method
    End Class
    
    Local server = HttpServer()
    server.Routes(Scores())
    Try
        server.Listen(SocketAddress(IpAddress.Loopback, 0))
        Local serving = server.Serve()
        Local base = "http://127.0.0.1:{server.LocalAddress.Port}"
        Local client = HttpClient()
    
        For entry In [Score("Ada", 1200), Score("Grace", 3400), Score("Alan", 2100),
                      Score("Linus", 900)]
            Local added = Await client.PostAsync("{base}/scores", Body.Json(Json.Write(entry)))
            Print("POST {entry.Player}: {added.Status}")
        End For
    
        Local best = Await client.GetAsync("{base}/scores?top=2")
        Print("GET /scores?top=2: {best.Status} {best.Text}")
        Local wrong = Await client.GetAsync("{base}/scores?top=lots")
        Print("GET /scores?top=lots: {wrong.Status} {wrong.Text}")
    
        server.Stop()
        Await serving
    Catch e As Error
        PrintError("failed: {e.Message}")
    End Try
    Output
    POST Ada: 201
    POST Grace: 201
    POST Alan: 201
    POST Linus: 201
    GET /scores?top=2: 200 [{"Player":"Grace","Points":3400},{"Player":"Alan","Points":2100}]
    GET /scores?top=lots: 400 Top: expected Int32, found "lots"

    ?top=2 fills Top, and without it, its default is 3. ?top=lots isn’t an Int, so the server answers 400 with the reason, and the route’s code never runs. StatusCode names the status codes, like StatusCode.CREATED for 201.

    A path parameter

    A :name part of a route’s path is a path parameter, which fills the argument’s field of that name. A route can answer with any status code. This one answers 404 for a player with no scores, and 400 for a score below zero. Best stays as it was, so the service now has all three routes.

    Import std.core
    Import std.collections
    Import std.data
    Import std.net
    Import std.net.http
    
    Struct Score
        Field Player: String
        Field Points: Int
    End Struct
    
    Struct NewScore
        @JsonBody Field Score: Score
    End Struct
    
    Struct Top
        Field Top: Int = 3
    End Struct
    
    Struct ByPlayer
        Field Player: String
    End Struct
    
    Class Scores
        Field Board: Score[] = []
    
        @Post("/scores")
        Async Method Add(args: NewScore): HttpResponse
            If args.Score.Points < 0
                Return HttpResponse.Of(StatusCode.BAD_REQUEST, Body.Text("Points can't be negative"))
            End If
            Board.Add(args.Score)
            Board.SortByDescending(Function(s: Score) => s.Points)
            Return HttpResponse.Of(StatusCode.CREATED, Body.Text("added"))
        End Method
    
        @Get("/scores")
        Async Method Best(args: Top): HttpResponse
            Local best = Board.View(0, Min(args.Top, Board.Count)).ToArray()
            Return HttpResponse.Of(Body.Json(Json.Write(best)))
        End Method
    
        @Get("/scores/:player")
        Async Method OfPlayer(args: ByPlayer): HttpResponse
            Local found = Board.Find(Function(s: Score) => s.Player == args.Player)
            If found == Null
                Return HttpResponse.Of(StatusCode.NOT_FOUND, Body.Text("no scores for {args.Player}"))
            End If
            Return HttpResponse.Of(Body.Json(Json.Write(found)))
        End Method
    End Class
    
    Local server = HttpServer()
    server.Routes(Scores())
    Try
        server.Listen(SocketAddress(IpAddress.Loopback, 0))
        Local serving = server.Serve()
        Local base = "http://127.0.0.1:{server.LocalAddress.Port}"
        Local client = HttpClient()
    
        For entry In [Score("Ada", 1200), Score("Grace", 3400), Score("Ada", 2100), Score("Eve", -5)]
            Local added = Await client.PostAsync("{base}/scores", Body.Json(Json.Write(entry)))
            Print("POST {entry.Player} {entry.Points}: {added.Status} {added.Text}")
        End For
    
        Local ada = Await client.GetAsync("{base}/scores/Ada")
        Print("GET /scores/Ada: {ada.Status} {ada.Text}")
        Local linus = Await client.GetAsync("{base}/scores/Linus")
        Print("GET /scores/Linus: {linus.Status} {linus.Text}")
        Local best = Await client.GetAsync("{base}/scores?top=2")
        Print("GET /scores?top=2: {best.Status} {best.Text}")
    
        server.Stop()
        Await serving
    Catch e As Error
        PrintError("failed: {e.Message}")
    End Try
    Output
    POST Ada 1200: 201 added
    POST Grace 3400: 201 added
    POST Ada 2100: 201 added
    POST Eve -5: 400 Points can't be negative
    GET /scores/Ada: 200 {"Player":"Ada","Points":2100}
    GET /scores/Linus: 404 no scores for Linus
    GET /scores?top=2: 200 [{"Player":"Grace","Points":3400},{"Player":"Ada","Points":2100}]

    The board is sorted best first, so Find gives each player’s best score. It gives a Score?, Null when no score matches, and after If found == Null returns, the compiler knows found holds a Score.

    Running it for real

    A service that runs until it’s stopped listens on a port of its own and waits on Serve(). This one has all three routes, and stops when the process is ended, as with Ctrl+C.

    Import std.core
    Import std.collections
    Import std.data
    Import std.net
    Import std.net.http
    
    Struct Score
        Field Player: String
        Field Points: Int
    End Struct
    
    Struct NewScore
        @JsonBody Field Score: Score
    End Struct
    
    Struct Top
        Field Top: Int = 3
    End Struct
    
    Struct ByPlayer
        Field Player: String
    End Struct
    
    Class Scores
        Field Board: Score[] = []
    
        @Post("/scores")
        Async Method Add(args: NewScore): HttpResponse
            If args.Score.Points < 0
                Return HttpResponse.Of(StatusCode.BAD_REQUEST, Body.Text("Points can't be negative"))
            End If
            Board.Add(args.Score)
            Board.SortByDescending(Function(s: Score) => s.Points)
            Return HttpResponse.Of(StatusCode.CREATED, Body.Text("added"))
        End Method
    
        @Get("/scores")
        Async Method Best(args: Top): HttpResponse
            Local best = Board.View(0, Min(args.Top, Board.Count)).ToArray()
            Return HttpResponse.Of(Body.Json(Json.Write(best)))
        End Method
    
        @Get("/scores/:player")
        Async Method OfPlayer(args: ByPlayer): HttpResponse
            Local found = Board.Find(Function(s: Score) => s.Player == args.Player)
            If found == Null
                Return HttpResponse.Of(StatusCode.NOT_FOUND, Body.Text("no scores for {args.Player}"))
            End If
            Return HttpResponse.Of(Body.Json(Json.Write(found)))
        End Method
    End Class
    
    Local server = HttpServer()
    server.Routes(Scores())
    Try
        server.Listen(SocketAddress(IpAddress.Any, 8080))
        Print("listening on port 8080")
        Await server.Serve()
    Catch e As Error
        PrintError("failed: {e.Message}")
    End Try

    While it runs, try it from another terminal:

    curl -X POST localhost:8080/scores -d '{"Player": "Ada", "Points": 1200}'
    curl localhost:8080/scores
    curl localhost:8080/scores/Ada

    fb build --release main.fb makes it one executable, with std’s HTTP code linked in, so it runs on a server with nothing installed beside it. The scores live in memory and go when the process ends; std.db’s SQLite would keep them in a file. std.net.http also has route groups, middleware, static files, WebSockets, and HTTPS.

    What you learned

    • An Async Method with @Get or @Post is a route, and server.Routes adds a Class’s routes.
    • A route’s argument Struct is filled from the path, the query, and a @JsonBody, and a value that doesn’t fit is a 400 before the route runs.
    • Json.Write turns Structs into JSON for a response.
    • Handlers are Async code on the main thread, and Await waits without blocking it.
    • Listen and Serve run a service, and fb build --release ships it as one file.

    Report an issue with this page or with FutureBlitz on GitHub.