FireflyFirefly
DocsGuides
GitHub

Getting Started

This guide walks you through creating your first Firefly application from scratch.

Prerequisites

Create a New Project

The Firefly CLI scaffolds a complete project with routing, configuration, and tests:

firefly new MyApp
cd MyApp

This generates:

MyApp/
  MyApp.sln
  src/MyApp/
    App.fs          # Entry point
    Router.fs       # Route definitions
    Endpoint.fs     # Handler functions
    Config/
      Dev.fs        # Development config
      Prod.fs       # Production config
    MyApp.fsproj
  tests/MyApp.Tests/
    Fixtures.fs
    IntegrationTests.fs
    ControllerTests.fs
    MyApp.Tests.fsproj

Run in Development Mode

firefly dev

This starts the server with dotnet watch run, enabling live reload and auto-restart on file changes. The environment is set to Development automatically.

Your First App from Scratch

If you prefer to start from an empty project:

open Firefly
open System.Threading
 
[<EntryPoint>]
let main _ =
    let routes =
        Route.start
        |> Route.get "/" (fun _ -> task {
            return Response.text "Hello, Firefly!"
        })
 
    App.run routes App.defaults CancellationToken.None
    |> Async.AwaitTask
    |> Async.RunSynchronously
    0

Core Concepts

Routes

Routes are built by piping through Route.start:

let routes =
    Route.start
    |> Route.get "/hello" (fun _ -> task { return Response.text "Hello" })
    |> Route.post "/users" (fun (req: Request) -> task {
        let! body = req.Json<CreateUser>()
        return Response.json body |> Response.status 201
    })

Request and Response

Every handler is a function that takes a Request and returns a Task<Response>:

type Handler = Request -> Task<Response>

The Request gives you access to:

req.Path          // string — URL path
req.Method        // string — HTTP method
req.Params        // IReadOnlyDictionary — route parameters
req.Query         // IReadOnlyDictionary — query string
req.Header "name" // string option
req.Cookie "name" // string option
req.Json<'T>()    // Task<'T> — parse JSON body
req.Text()        // Task<string> — raw body text
req.Form()        // Task<IReadOnlyDictionary> — form data
req.Files()       // Task<UploadedFile list> — uploaded files
req.RequestId     // string option
req.CorrelationId // string option
req.Accepts "type" // bool — content negotiation
req.ContentType   // string option
req.Raw           // HttpContext — escape hatch

Build responses with the Response module:

Response.text "plain text"
Response.json {| name = "Firefly" |}
Response.html "<h1>Hello</h1>"
Response.ok                        // 200 empty
Response.created                   // 201 empty
Response.noContent                 // 204 empty
Response.notFound                  // 404 empty
Response.unauthorized              // 401 empty
Response.file "path/to/file.pdf"
Response.stream someStream
 
// Chainable modifiers
Response.json data
|> Response.status 201
|> Response.header "X-Custom" "value"
|> Response.cookie "session" "abc123"
|> Response.etag "\"v1\""
|> Response.cacheControl "public, max-age=3600"

Configuration

Configure the server via the App module:

let config =
    App.defaults
    |> App.port 8080
    |> App.host "0.0.0.0"
    |> App.onError (fun ex req -> task {
        return Response.json {| error = ex.Message |} |> Response.status 500
    })
    |> App.notFound (fun req -> task {
        return Response.json {| error = "Not found" |} |> Response.status 404
    })
    |> App.shutdownTimeout (System.TimeSpan.FromSeconds 30.0)
 
App.run routes config CancellationToken.None

Middleware

Apply middleware globally or per-route:

// Global — applies to all routes
let config =
    App.defaults
    |> App.middleware Cors.allowAll
    |> App.middleware SecureHeaders.middleware
 
// Per-route group
Route.start
|> Route.group "/api" (fun t ->
    t
    |> Route.middleware (Jwt.validate jwtConfig)
    |> Route.get "/profile" profileHandler
)

Next Steps