From 01e0b22e5e0f0534d68efc2f25befae7172d522d Mon Sep 17 00:00:00 2001 From: Hayleigh Thompson Date: Mon, 26 Feb 2024 20:21:41 +0000 Subject: :memo Move docs bages into pages/ dir. --- README.md | 4 +- docs/guide/01-quickstart.md | 384 ------------------------------------------- gleam.toml | 2 +- pages/guide/01-quickstart.md | 384 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 387 insertions(+), 387 deletions(-) delete mode 100644 docs/guide/01-quickstart.md create mode 100644 pages/guide/01-quickstart.md diff --git a/README.md b/README.md index e297d56..fa640ca 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ | --> - + Quickstart | @@ -165,7 +165,7 @@ end ## Where next -To get up to speed with Lustre, check out the [quickstart guide](./guide/01-quickstart). +To get up to speed with Lustre, check out the [quickstart guide](https://hexdocs.pm/lustre/4.0.0-rc1/guide/01-quickstart). If you prefer to see some code, the [examples](https://github.com/lustre-labs/lustre/tree/main/examples) directory contains a handful of small applications that demonstrate different aspects of the framework. diff --git a/docs/guide/01-quickstart.md b/docs/guide/01-quickstart.md deleted file mode 100644 index b0b61fb..0000000 --- a/docs/guide/01-quickstart.md +++ /dev/null @@ -1,384 +0,0 @@ -# 01 Quickstart Guide - -Welcome to the Lustre quickstart guide! This document should get you up to speed -with the core ideas that underpin every Lustre application as well as how to get -something on the screen. - -## What is a SPA? - -Lustre can be used to create HTML in many different contexts, but it is primarily -designed to be used to build Single-Page Applications – or SPAs. SPAs are a type -of Web application that render content primarily in the browser (rather than on -the server) and, crucially, do not require a full page load when navigating -between pages or loading new content. - -To help build these kinds of applications, Lustre comes with an opinionated -runtime. Some of Lustre's core features include: - -- **Declarative rendering**: User interfaces are constructed using a declarative - API that describes HTML as a function of your application's state. This is in - contrast to more traditional imperative approaches to direct DOM mutation like - jQuery. - -- **State management**: If UIs are a function of state, then orchestrating state - changes is crucial! Lustre provides a simple message-based state management - system modelled after OTP [gen_servers](https://www.erlang.org/doc/design_principles/gen_server_concepts), - Gleam's [actors](https://hexdocs.pm/gleam_otp/gleam/otp/actor.html), and the - [Elm Architecture](https://guide.elm-lang.org/architecture/). - -- **Managed side effects**: Managing asynchronous operations like HTTP requests - and timers can be tricky when JavaScript is single-threaded. Lustre provides a - runtime to manage these side effects and let them communicate with your application - using the same messages as your update loop. - -## Your first Lustre program - -To get started, let's create a new Gleam application and add Lustre as a dependency. - -```sh -$ gleam new app && cd app && gleam add lustre -``` - -By default, Gleam builds projects for the Erlang target unless told otherwise. We -can change this by adding a `target` field to the `gleam.toml` file generated in -the root of the project. - -```diff - name = "app" -+ target = "javascript" - version = "1.0.0" - - ... -``` - -The simplest type of Lustre application is constructed with the `element` function. -This produces an application that renders a static piece of content without the -typical update loop. - -We can start by importing `lustre` and `lustre/element` and just rendering some -text: - -```gleam -import lustre -import lustre/element - -pub fn main() { - lustre.element(element.text("Hello, world!")) -} -``` - -Lustre includes tooling like a server to serve your application in development. -You can start that server by running: - -```sh -$ gleam run -m lustre dev -``` - -The first time you run this command might take a little while, but subsequent runs -should be much faster! - -> **Note**: Lustre uses esbuild under the hood, and attempts to download the [right -> binary for your platform](https://esbuild.github.io/getting-started/#download-a-build). -> If you're not connected to the internet, on an unsupported platform, or don't -> want Lustre to download the binary you can grab or build it yourself and place it -> in `build/.lustre/bin/esbuild`. - -Once the server is up and running you should be able to visit http://localhost:1234 -and be greeted with your "Hello, world!" message. - -We mentioned Lustre has a declarative API for constructing HTML. Let's see what -that looks like by building something slightly more complex. - -```gleam -import lustre -import lustre/attribute -import lustre/element -import lustre/element/html - -pub fn main() { - lustre.element( - html.div([], [ - html.h1([], [element.text("Hello, world!")]), - html.figure([], [ - html.img([attribute.src("https://cataas.com/cat")]), - html.figcaption([], [element.text("A cat!")]) - ]) - ]) - ) -} -``` - -Here we _describe_ the structure of the HTML we want to render, and leave the -busywork to Lustre's runtime: that's what makes it declarative! - -"**Where are the templates?**" we hear you cry. Lustre doesn't have a separate -templating syntax like JSX or HEEx for a few reasons (lack of metaprogramming -built into Gleam, for one). Some folks might find this a bit odd at first, but -we encourage you to give it a try. Realising that your UI is _just functions_ -can be a bit of a lightbulb moment as you build more complex applications. - -## Adding interactivity - -Rendering static HTML is great, but we said at the beginning Lustre was designed -primarily for building SPAs – and SPAs are interactive! To do that we'll need -to move on from `lustre.element` to the first of Lustre's application constructors -that includes an update loop: `lustre.simple`. - -```gleam -import gleam/int -import lustre -import lustre/element -import lustre/element/html -import lustre/event - -pub fn main() { - lustre.simple(init, update, view) -} -``` - -There are three main building blocks to every interactive Lustre application: - -- A `Model` that represents your application's state and an `init` function - to create it. - -- A `Msg` type that represents all the different ways the outside world can - communicate with your application and an `update` function that modifies - your model in response to those messages. - -- A `view` function that renders your model to HTML. - -We'll build a simple counter application to demonstrate these concepts. Our -model can be an `Int` and our `init` function will initialise it to `0`: - -```gleam -pub type Model = Int - -fn init(_) -> Model { - 0 -} -``` - -> **Note**: The `init` function always takes a single argument! These are the "flags" -> or start arguments you can pass in when your application is started with -> `lustre.start`. For the time being, we can ignore them, but they're useful for -> passing in configuration or other data when your application starts. - -The main update loop in a Lustre application revolves around messages passed in -from the outside world. For our counter application, we'll have two messages to -increment and decrement the counter: - -```gleam -pub type Msg { - Increment - Decrement -} - -pub fn update(model: Model, msg: Msg) -> Model { - case msg { - Increment -> model + 1 - Decrement -> model - 1 - } -} -``` - -Each time a message is produced from an event listener, Lustre will call your -`update` function with the current model and the incoming message. The result -will be the new application state that is then passed to the `view` function: - -```gleam -pub fn view(model: Model) -> lustre.Element(Msg) { - let count = int.to_string(model) - - html.div([], [ - html.button([event.click(Increment)], [ - element.text("+") - ]), - element.text(count), - html.button([event.click(Decrement)], [ - element.text("-") - ]) - ]) -} -``` - -The above snippet attaches two click event listeners that produce an `Increment` -or `Decrement` message when clicked. The Lustre runtime is responsible for -attaching these event listeners and calling your `update` function with the -resulting message. - -> **Note**: notice that the return type of `view` is `lustre.Element(Msg)`. The -> type parameter `Msg` tells us the kinds of messages this element might produce -> from events: type safety to the rescue! - -This forms the core of every Lustre application: - -- A model produces some view. -- The view can produce messages in response to user interaction. -- Those messages are passed to the update function to produce a new model. -- ... and the cycle continues. - -## Talking to the outside world - -This "closed loop" of messages and updates works well if all we need is an -interactive document, but many applications will also need to talk to the outside -world – whether that's fetching data from an API, setting up a WebSocket connection, -or even just setting a timer. - -Lustre manages these side effects through an abstraction called an `Effect`. In -essence, effects are any functions that talk with the outside world and might -want to send messages back to your application. Lustre lets you write your own -effects, but for now we'll use a community package called -[`lustre_http`](https://hexdocs.pm/lustre_http/index.html) to fetch a new cat image -every time the counter is incremented. - -Because this is a separate package, make sure to add it to your project first. -While we're here, we'll also add `gleam_json` so we can decode the response from -the cat API: - -```sh -$ gleam add gleam_json lustre_http -``` - -Now we are introducing side effects, we need to graduate from `lustre.simple` to -the more powerful `lustre.application` constructor. - -```gleam -import gleam/int -import lustre -import lustre_http -import lustre/attribute -import lustre/element -import lustre/element/html -import lustre/event - -pub fn main() { - lustre.application(init, update, view) -} -``` - -If you edited your previous counter app, you'll notice the program no longer -compiles. Specifically, the type of our `init` and `update` functions are wrong -for the new `lustre.application` constructor! - -In order to tell Lustre about what effects it should perform, these functions now -need to return a _tuple_ of the new model and any effects. We can amend our `init` -function like so: - -```gleam -pub type Model { - Model(count: Int, cats: List(String)) -} - -fn init(_) -> #(Model, effect.Effect(Msg)) { - #(Model(0, []), effect.none()) -} -``` - -The `effect.none` function is a way of saying "no effects" – we don't need to do -anything when the application starts. We've also changed our `Model` type from a -simple type alias to a Gleam [record](https://tour.gleam.run/data-types/records/) -that holds both the current count and a list of cat image URLs. - -In our `update` function, we want to fetch a new cat image every time the counter -is incremented. To do this we need two things: - -- An `Effect` to describe the request the runtime should perform. -- A variant of our `Msg` to handle the response. - -The `lustre_http` package has the effect side of things handled, so we just need -to modify our `Msg` type to include a new variant for the response: - -```gleam -pub type Msg { - Increment - Decrement - GotCat(Result(String, lustre_http.HttpError)) -} -``` - -Finally, we can modify our `update` function to also fetch a cat image when the -counter is incremented and handle the response: - -```gleam -pub fn update(model: Model, msg: Msg) -> #(Model, effect.Effect(Msg)) { - case msg { - Increment -> #(Model(..model, count: model.count + 1), get_cat()) - Decrement -> #(Model(..model, count: model.count - 1), effect.none()) - GotCat(Ok(cat)) -> #(Model(..model, cats: [cat, ..model.cats]), effect.none()) - GotCat(Error(_)) -> #(model, effect.none()) - } -} - -fn get_cat() -> effect.Effect(Msg) { - let decoder = dynamic.field("_id", dynamic.string) - let expect = lustre_http.expect_json(decoder, GotCat) - - lustre_http.get("https://cataas.com/cat?json=true", expect) -} -``` - -> **Note**: The `get_cat` function returns an `Effect` that tells the runtime how -> to fetch a cat image. It's important to know that the `get_cat` function doesn't -> perform the request directly! This is why we need to add the `GotCat` message -> variant: the runtime needs to know what to do with the response when it arrives. - -This model of managed effects can feel cumbersome at first, but it comes with some -benefits. Forcing side effects to produce a message means our message type naturally -describes all the ways the world can communicate with our application; as an app -grows being able to get this kind of overview is invaluable! It also means we can -test our update loop in isolation from the runtime and side effects: we could write -tests that verify a particular sequence of messages produces an expected model -without needing to mock out HTTP requests or timers. - -Before we forget, let's also update our `view` function to actually display the -cat images we're fetching: - -```gleam -pub fn view(model: Model) -> lustre.Element(Msg) { - let count = int.to_string(model.count) - - html.div([], [ - html.button([event.click(Increment)], [ - element.text("+") - ]), - element.text(count), - html.button([event.click(Decrement)], [ - element.text("-") - ]), - html.div([], list.map(model.cats, fn(cat) { - html.img([attribute.src(cat)]) - })) - ]) -} -``` - -## Where to go from here - -Believe it or not, you've already seen about 80% of what Lustre has to offer! From -these core concepts, you can build rich interactive applications that are predictable -and maintainable. Where to go from here depends on what you want to build, and -how you like to learn: - -- There are a number of [examples](https://github.com/lustre-labs/lustre/tree/main/examples) - if the Lustre repository that gradually introduce more complex applications - and ideas. - -- The [rest of this guide](/guide/02-state-management) also continues to teach - Lustre's high-level concepts and best-practices. - -- If you're coming from LiveView or have heard about Lustre's server components - and want to learn more, you can skip to the [server components](/guide/05-server-components) - section of the guide to learn about how to run Lustre applications on the backend. - -- Of course, if you want to dive in and start making things straight away, the - [API documentation](/lustre) is always handy to keep open. - -## Getting help - -If you're having trouble with Lustre or not sure what the right way to do -something is, the best place to get help is the [Gleam Discord server](https://discord.gg/Fm8Pwmy). -You could also open an issue on the [Lustre GitHub repository](https://github.com/lustre-labs/lustre/issues). - -While our docs are still a work in progress, the official [Elm guide](https://guide.elm-lang.org) -is also a great resource for learning about the Model-View-Update architecture -and the kinds of patterns that Lustre is built around. diff --git a/gleam.toml b/gleam.toml index 94cf365..43b2689 100644 --- a/gleam.toml +++ b/gleam.toml @@ -23,7 +23,7 @@ internal_modules = [ pages = [ # { title = "CLI reference", path = "#", source = "" }, # { title = " ", path = "#", source = "" }, - { title = "Quickstart guide", path = "guide/01-quickstart.html", source = "./docs/guide/01-quickstart.md" }, + { title = "Quickstart guide", path = "guide/01-quickstart.html", source = "./pages/guide/01-quickstart.md" }, # { title = "Managing state", path = "#", source = "" }, # { title = "Side effects", path = "#", source = "" }, # { title = "Server-side rendering", path = "#", source = "" }, diff --git a/pages/guide/01-quickstart.md b/pages/guide/01-quickstart.md new file mode 100644 index 0000000..b0b61fb --- /dev/null +++ b/pages/guide/01-quickstart.md @@ -0,0 +1,384 @@ +# 01 Quickstart Guide + +Welcome to the Lustre quickstart guide! This document should get you up to speed +with the core ideas that underpin every Lustre application as well as how to get +something on the screen. + +## What is a SPA? + +Lustre can be used to create HTML in many different contexts, but it is primarily +designed to be used to build Single-Page Applications – or SPAs. SPAs are a type +of Web application that render content primarily in the browser (rather than on +the server) and, crucially, do not require a full page load when navigating +between pages or loading new content. + +To help build these kinds of applications, Lustre comes with an opinionated +runtime. Some of Lustre's core features include: + +- **Declarative rendering**: User interfaces are constructed using a declarative + API that describes HTML as a function of your application's state. This is in + contrast to more traditional imperative approaches to direct DOM mutation like + jQuery. + +- **State management**: If UIs are a function of state, then orchestrating state + changes is crucial! Lustre provides a simple message-based state management + system modelled after OTP [gen_servers](https://www.erlang.org/doc/design_principles/gen_server_concepts), + Gleam's [actors](https://hexdocs.pm/gleam_otp/gleam/otp/actor.html), and the + [Elm Architecture](https://guide.elm-lang.org/architecture/). + +- **Managed side effects**: Managing asynchronous operations like HTTP requests + and timers can be tricky when JavaScript is single-threaded. Lustre provides a + runtime to manage these side effects and let them communicate with your application + using the same messages as your update loop. + +## Your first Lustre program + +To get started, let's create a new Gleam application and add Lustre as a dependency. + +```sh +$ gleam new app && cd app && gleam add lustre +``` + +By default, Gleam builds projects for the Erlang target unless told otherwise. We +can change this by adding a `target` field to the `gleam.toml` file generated in +the root of the project. + +```diff + name = "app" ++ target = "javascript" + version = "1.0.0" + + ... +``` + +The simplest type of Lustre application is constructed with the `element` function. +This produces an application that renders a static piece of content without the +typical update loop. + +We can start by importing `lustre` and `lustre/element` and just rendering some +text: + +```gleam +import lustre +import lustre/element + +pub fn main() { + lustre.element(element.text("Hello, world!")) +} +``` + +Lustre includes tooling like a server to serve your application in development. +You can start that server by running: + +```sh +$ gleam run -m lustre dev +``` + +The first time you run this command might take a little while, but subsequent runs +should be much faster! + +> **Note**: Lustre uses esbuild under the hood, and attempts to download the [right +> binary for your platform](https://esbuild.github.io/getting-started/#download-a-build). +> If you're not connected to the internet, on an unsupported platform, or don't +> want Lustre to download the binary you can grab or build it yourself and place it +> in `build/.lustre/bin/esbuild`. + +Once the server is up and running you should be able to visit http://localhost:1234 +and be greeted with your "Hello, world!" message. + +We mentioned Lustre has a declarative API for constructing HTML. Let's see what +that looks like by building something slightly more complex. + +```gleam +import lustre +import lustre/attribute +import lustre/element +import lustre/element/html + +pub fn main() { + lustre.element( + html.div([], [ + html.h1([], [element.text("Hello, world!")]), + html.figure([], [ + html.img([attribute.src("https://cataas.com/cat")]), + html.figcaption([], [element.text("A cat!")]) + ]) + ]) + ) +} +``` + +Here we _describe_ the structure of the HTML we want to render, and leave the +busywork to Lustre's runtime: that's what makes it declarative! + +"**Where are the templates?**" we hear you cry. Lustre doesn't have a separate +templating syntax like JSX or HEEx for a few reasons (lack of metaprogramming +built into Gleam, for one). Some folks might find this a bit odd at first, but +we encourage you to give it a try. Realising that your UI is _just functions_ +can be a bit of a lightbulb moment as you build more complex applications. + +## Adding interactivity + +Rendering static HTML is great, but we said at the beginning Lustre was designed +primarily for building SPAs – and SPAs are interactive! To do that we'll need +to move on from `lustre.element` to the first of Lustre's application constructors +that includes an update loop: `lustre.simple`. + +```gleam +import gleam/int +import lustre +import lustre/element +import lustre/element/html +import lustre/event + +pub fn main() { + lustre.simple(init, update, view) +} +``` + +There are three main building blocks to every interactive Lustre application: + +- A `Model` that represents your application's state and an `init` function + to create it. + +- A `Msg` type that represents all the different ways the outside world can + communicate with your application and an `update` function that modifies + your model in response to those messages. + +- A `view` function that renders your model to HTML. + +We'll build a simple counter application to demonstrate these concepts. Our +model can be an `Int` and our `init` function will initialise it to `0`: + +```gleam +pub type Model = Int + +fn init(_) -> Model { + 0 +} +``` + +> **Note**: The `init` function always takes a single argument! These are the "flags" +> or start arguments you can pass in when your application is started with +> `lustre.start`. For the time being, we can ignore them, but they're useful for +> passing in configuration or other data when your application starts. + +The main update loop in a Lustre application revolves around messages passed in +from the outside world. For our counter application, we'll have two messages to +increment and decrement the counter: + +```gleam +pub type Msg { + Increment + Decrement +} + +pub fn update(model: Model, msg: Msg) -> Model { + case msg { + Increment -> model + 1 + Decrement -> model - 1 + } +} +``` + +Each time a message is produced from an event listener, Lustre will call your +`update` function with the current model and the incoming message. The result +will be the new application state that is then passed to the `view` function: + +```gleam +pub fn view(model: Model) -> lustre.Element(Msg) { + let count = int.to_string(model) + + html.div([], [ + html.button([event.click(Increment)], [ + element.text("+") + ]), + element.text(count), + html.button([event.click(Decrement)], [ + element.text("-") + ]) + ]) +} +``` + +The above snippet attaches two click event listeners that produce an `Increment` +or `Decrement` message when clicked. The Lustre runtime is responsible for +attaching these event listeners and calling your `update` function with the +resulting message. + +> **Note**: notice that the return type of `view` is `lustre.Element(Msg)`. The +> type parameter `Msg` tells us the kinds of messages this element might produce +> from events: type safety to the rescue! + +This forms the core of every Lustre application: + +- A model produces some view. +- The view can produce messages in response to user interaction. +- Those messages are passed to the update function to produce a new model. +- ... and the cycle continues. + +## Talking to the outside world + +This "closed loop" of messages and updates works well if all we need is an +interactive document, but many applications will also need to talk to the outside +world – whether that's fetching data from an API, setting up a WebSocket connection, +or even just setting a timer. + +Lustre manages these side effects through an abstraction called an `Effect`. In +essence, effects are any functions that talk with the outside world and might +want to send messages back to your application. Lustre lets you write your own +effects, but for now we'll use a community package called +[`lustre_http`](https://hexdocs.pm/lustre_http/index.html) to fetch a new cat image +every time the counter is incremented. + +Because this is a separate package, make sure to add it to your project first. +While we're here, we'll also add `gleam_json` so we can decode the response from +the cat API: + +```sh +$ gleam add gleam_json lustre_http +``` + +Now we are introducing side effects, we need to graduate from `lustre.simple` to +the more powerful `lustre.application` constructor. + +```gleam +import gleam/int +import lustre +import lustre_http +import lustre/attribute +import lustre/element +import lustre/element/html +import lustre/event + +pub fn main() { + lustre.application(init, update, view) +} +``` + +If you edited your previous counter app, you'll notice the program no longer +compiles. Specifically, the type of our `init` and `update` functions are wrong +for the new `lustre.application` constructor! + +In order to tell Lustre about what effects it should perform, these functions now +need to return a _tuple_ of the new model and any effects. We can amend our `init` +function like so: + +```gleam +pub type Model { + Model(count: Int, cats: List(String)) +} + +fn init(_) -> #(Model, effect.Effect(Msg)) { + #(Model(0, []), effect.none()) +} +``` + +The `effect.none` function is a way of saying "no effects" – we don't need to do +anything when the application starts. We've also changed our `Model` type from a +simple type alias to a Gleam [record](https://tour.gleam.run/data-types/records/) +that holds both the current count and a list of cat image URLs. + +In our `update` function, we want to fetch a new cat image every time the counter +is incremented. To do this we need two things: + +- An `Effect` to describe the request the runtime should perform. +- A variant of our `Msg` to handle the response. + +The `lustre_http` package has the effect side of things handled, so we just need +to modify our `Msg` type to include a new variant for the response: + +```gleam +pub type Msg { + Increment + Decrement + GotCat(Result(String, lustre_http.HttpError)) +} +``` + +Finally, we can modify our `update` function to also fetch a cat image when the +counter is incremented and handle the response: + +```gleam +pub fn update(model: Model, msg: Msg) -> #(Model, effect.Effect(Msg)) { + case msg { + Increment -> #(Model(..model, count: model.count + 1), get_cat()) + Decrement -> #(Model(..model, count: model.count - 1), effect.none()) + GotCat(Ok(cat)) -> #(Model(..model, cats: [cat, ..model.cats]), effect.none()) + GotCat(Error(_)) -> #(model, effect.none()) + } +} + +fn get_cat() -> effect.Effect(Msg) { + let decoder = dynamic.field("_id", dynamic.string) + let expect = lustre_http.expect_json(decoder, GotCat) + + lustre_http.get("https://cataas.com/cat?json=true", expect) +} +``` + +> **Note**: The `get_cat` function returns an `Effect` that tells the runtime how +> to fetch a cat image. It's important to know that the `get_cat` function doesn't +> perform the request directly! This is why we need to add the `GotCat` message +> variant: the runtime needs to know what to do with the response when it arrives. + +This model of managed effects can feel cumbersome at first, but it comes with some +benefits. Forcing side effects to produce a message means our message type naturally +describes all the ways the world can communicate with our application; as an app +grows being able to get this kind of overview is invaluable! It also means we can +test our update loop in isolation from the runtime and side effects: we could write +tests that verify a particular sequence of messages produces an expected model +without needing to mock out HTTP requests or timers. + +Before we forget, let's also update our `view` function to actually display the +cat images we're fetching: + +```gleam +pub fn view(model: Model) -> lustre.Element(Msg) { + let count = int.to_string(model.count) + + html.div([], [ + html.button([event.click(Increment)], [ + element.text("+") + ]), + element.text(count), + html.button([event.click(Decrement)], [ + element.text("-") + ]), + html.div([], list.map(model.cats, fn(cat) { + html.img([attribute.src(cat)]) + })) + ]) +} +``` + +## Where to go from here + +Believe it or not, you've already seen about 80% of what Lustre has to offer! From +these core concepts, you can build rich interactive applications that are predictable +and maintainable. Where to go from here depends on what you want to build, and +how you like to learn: + +- There are a number of [examples](https://github.com/lustre-labs/lustre/tree/main/examples) + if the Lustre repository that gradually introduce more complex applications + and ideas. + +- The [rest of this guide](/guide/02-state-management) also continues to teach + Lustre's high-level concepts and best-practices. + +- If you're coming from LiveView or have heard about Lustre's server components + and want to learn more, you can skip to the [server components](/guide/05-server-components) + section of the guide to learn about how to run Lustre applications on the backend. + +- Of course, if you want to dive in and start making things straight away, the + [API documentation](/lustre) is always handy to keep open. + +## Getting help + +If you're having trouble with Lustre or not sure what the right way to do +something is, the best place to get help is the [Gleam Discord server](https://discord.gg/Fm8Pwmy). +You could also open an issue on the [Lustre GitHub repository](https://github.com/lustre-labs/lustre/issues). + +While our docs are still a work in progress, the official [Elm guide](https://guide.elm-lang.org) +is also a great resource for learning about the Model-View-Update architecture +and the kinds of patterns that Lustre is built around. -- cgit v1.2.3