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 Try200 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 TryPOST 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 TryPOST 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 TryWhile 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/Adafb 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
@Getor@Postis a route, andserver.Routesadds 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.Writeturns Structs into JSON for a response.- Handlers are Async code on the main thread, and
Awaitwaits without blocking it. ListenandServerun a service, andfb build --releaseships it as one file.
Report an issue with this page or with FutureBlitz on GitHub.