
<!-- path: tutorial/index.md -->
# Tutorial

Here are some tutorials that may help you learn the programming language:

## General

- [An interactive tour with language basics](https://tour.moonbitlang.com)
- [Tour for Beginners](tour.md)
- [Native CLI Quickstart](cli-quickstart.md)
- [Fullstack in One MoonBit Project](fullstack-one-project.md)

## Language Transition Guides

- [MoonBit for Go Programmers](for-go-programmers/index.md)

<!-- path: tutorial/tour.md -->
## A Tour of MoonBit for Beginners

This guide is intended for newcomers, and it's not meant to be a 5-minute quick
tour. This article tries to be a succinct yet easy to understand guide for those
who haven't programmed in a way that MoonBit enables them to, that is, in a more
modern, functional way.

See [the General Introduction](../language/index.md) if you want to straight
delve into the language.

### Installation

**The extension**

Currently, MoonBit development support is through the VS Code extension.
Navigate to
[VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=moonbit.moonbit-lang)
to download MoonBit language support.

**The toolchain**

> (Recommended) If you've installed the extension above, the runtime can be
> directly installed by running 'Install moonbit toolchain' in the action menu
> and you may skip this part:
> ![runtime-installation](imgs/runtime-installation.png)

We also provide an installation script: Linux & macOS users can install via

```bash
curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash
```

For Windows users, PowerShell is used:

```powershell
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser; irm https://cli.moonbitlang.com/install/powershell.ps1 | iex
```

This automatically installs MoonBit in `$HOME/.moon` and adds it to your `PATH`.

If you encounter `moon` not found after installation, try restarting your
terminal or VS Code to let the environment variable take effect.

Do notice that MoonBit is not production-ready at the moment, it's under active
development. To update MoonBit, just run the commands above again.

Running `moon help` gives us a bunch of subcommands. But right now the only
commands we need are `build`, `run`, and `new`.

To create a project (or module, more formally), run `moon new <path>`, where path
is the place you would like to place the project. For example, if you execute
`moon new examine`, you will get:

```default
examine
├── Agents.md
├── cmd
│   └── main
│       ├── main.mbt
│       └── moon.pkg
├── LICENSE
├── moon.mod.json
├── moon.pkg
├── examine_test.mbt
├── examine.mbt
├── README.mbt.md
└── README.md -> README.mbt.md
```

which contains a `cmd/main` lib containing a `fn main` that serves as the entrance
of the program. Try running `cd examine && moon run cmd/main`.

In this tutorial, we assume the project name is `examine`,
and the current working directory is also `examine`.

### Example: Finding students who passed the test

In this example, we will try to find out, given the scores of some students, how
many of them have passed the test?

To do so, we will start with defining our data types, identify our functions,
and write our tests. Then we will implement our functions.

Unless specified, the following will be defined under the file `examine.mbt`.

#### Data types

The [basic data types](../language/fundamentals.md#built-in-data-structures) in MoonBit include the following:

- `Unit`
- `Bool`
- `Int`, `UInt`, `Int64`, `UInt64`, `Byte`, ...
- `Float`, `Double`
- `Char`, `String`, ...
- `Array[T]`, ...
- Tuples, and still others

To represent a struct containing a student ID and a score using a primitive
type, we can use a 2-tuple containing a student ID (of type `String`) and a
score (of type `Double`) as `(String, Double)`. However this is not very
intuitive as we can't distinguish with other possible data types, such as a struct
containing a student ID and the height of the student.

So we choose to declare our own data type using [struct](../language/fundamentals.md#struct):

```moonbit
struct Student {
  id : String
  score : Double
}
```

One can either pass or fail an exam, so the judgement result can be defined
using [enum](../language/fundamentals.md#enum):

```moonbit
enum ExamResult {
  Pass
  Fail
}
```

#### Functions

A [Function](../language/fundamentals.md#functions) is a piece of code that takes some inputs and produces a result.

In our example, we need to judge whether a student has passed an exam:

```moonbit
fn is_qualified(student : Student, criteria: Double) -> ExamResult {
  ...
}
```

This function takes an input `student` of type `Student` that we've just defined, an input `criteria` of type `Double` as the criteria may be different for each course or different in each country, and returns an `ExamResult`.

The `...` syntax allows us to leave functions unimplemented for now.

We also need to find out how many students have passed an exam:

```moonbit
fn count_qualified_students(
  students : Array[Student],
  is_qualified : (Student) -> ExamResult
) -> Int {
  ...
}
```

In MoonBit, functions are first-class, meaning that we can bind a function to a variable, pass a function as parameter or receive a function as a result.
This function takes an array of students' structs and another function that will judge whether a student has passed an exam.

#### Writing tests

We can define inline tests to define the expected behavior of the functions. This is also helpful to make sure that there'll be no regressions when we refactor the program.

```moonbit
test "is qualified" {
  assert_eq(is_qualified(Student::{ id : "0", score : 50.0 }, 60.0), Fail)
  assert_eq(is_qualified(Student::{ id : "1", score : 60.0 }, 60.0), Pass)
  assert_eq(is_qualified(Student::{ id : "2", score : 13.0 }, 7.0), Pass)
}
```

We will get an error message, reminding us that `Show` and `Eq` are not implemented for `ExamResult`.

`Show` and `Eq` are **traits**. A trait in MoonBit defines some common operations that a type should be able to perform.

For example, `Eq` defines that there should be a way to compare two values of the same type with a function called `op_equal`:

```moonbit
trait Eq {
  op_equal(Self, Self) -> Bool
}
```

and `Show` defines that there should be a way to either convert a value of a type into `String` or write it using a `Logger`:

```moonbit
trait Show {
  output(Self, &Logger) -> Unit
  to_string(Self) -> String
}
```

And the `assert_eq` uses them to constraint the passed parameters so that it can compare the two values and print them when they are not equal:

```moonbit
fn assert_eq![A : Eq + Show](value : A, other : A) -> Unit {
  ...
}
```

We need to implement `Eq` and `Show` for our `ExamResult`. There are two ways to do so.

1. By defining an explicit implementation:
   ```moonbit
   impl Eq for ExamResult with equal(self, other) {
     match (self, other) {
       (Pass, Pass) | (Fail, Fail) => true
       _ => false
     }
   }
   ```

   Here we use [pattern matching](../language/fundamentals.md#pattern-matching) to check the cases of the `ExamResult`.
2. An alternative way is by [deriving](../language/derive.md) since `Eq` and `Show` are [builtin traits](../language/methods.md#builtin-traits) and the output for `ExamResult` is quite straightforward:
   ```moonbit
   enum ExamResult {
     Pass
     Fail
   } derive(Show)
   ```

Now that we've implemented the traits, we can continue with our test implementations:

```moonbit
test "count qualified students" {
  let students = [
    { id: "0", score: 10.0 },
    { id: "1", score: 50.0 },
    { id: "2", score: 61.0 },
  ]
  let criteria1 = fn(student) { is_qualified(student, 10) }
  let criteria2 = fn(student) { is_qualified(student, 50) }
  assert_eq(count_qualified_students(students, criteria1), 3)
  assert_eq(count_qualified_students(students, criteria2), 2)
}
```

Here we use [lambda expressions](../language/fundamentals.md#local-functions) to reuse the previously defined `is_qualified` to create different criteria.

We can run `moon test` to see whether the tests succeed or not.

#### Implementing the functions

For the `is_qualified` function, it is as easy as a simple comparison:

```moonbit
fn is_qualified(student : Student, criteria : Double) -> ExamResult {
  if student.score >= criteria {
    Pass
  } else {
    Fail
  }
}
```

In MoonBit, the result of the last expression is the return value of the function, and the result of each branch is the value of the `if` expression.

For the `count_qualified_students` function, we need to iterate through the array to check if each student has passed or not.

A naive version is by using a mutable value and a [`for` loop](../language/fundamentals.md#for-loop):

```moonbit
fn count_qualified_students(
  students : Array[Student],
  is_qualified : (Student) -> ExamResult
) -> Int {
  let mut count = 0
  for i = 0; i < students.length(); i = i + 1 {
    if is_qualified(students[i]) == Pass {
      count += 1
    }
  }
  count
}
```

However, this is neither efficient (due to the border check) nor intuitive, so we can replace the `for` loop with a [`for .. in` loop](../language/fundamentals.md#for-in-loop):

```moonbit
fn count_qualified_students(
  students : Array[Student],
  is_qualified : (Student) -> ExamResult
) -> Int {
  let mut count = 0
  for student in students {
    if is_qualified(student) == Pass { count += 1}
  }
  count
}
```

Still another way is use the functions defined for [iterator](../language/fundamentals.md#iterator):

```moonbit
fn count_qualified_students(
  students : Array[Student],
  is_qualified : (Student) -> ExamResult
) -> Int {
  students.iter().filter(fn(student) { is_qualified(student) == Pass }).count()
}
```

Now the tests defined before should pass.

### Making the library available

Congratulation on your first MoonBit library!

You can now share it with other developers so that they don't need to repeat what you have done.

But before that, you have some other things to do.

#### Adjusting the visibility

To see how other people may use our program, MoonBit provides a mechanism called ["black box test"](../language/tests.md#blackbox-tests-and-whitebox-tests).

Let's move the `test` block we defined above into a new file `top_test.mbt`.

Oops! Now there are errors complaining that:

- `is_qualified` and `count_qualified_students` are unbound
- `Fail` and `Pass` are undefined
- `Student` is not a struct type and the field `id` is not found, etc.

All these come from the problem of visibility. By default, a function defined is not visible for other part of the program outside the current package (bound by the current folder).
And by default, a type is viewed as an abstract type, i.e. we know only that there exists a type `Student` and a type `ExamResult`. By using the black box test, you can make sure that
everything you'd like others to have is indeed decorated with the intended visibility.

In order for others to use the functions, we need to add `pub` before the `fn` to make the function public.

In order for others to construct the types and read the content, we need to add `pub(all)` before the `struct` and `enum` to make the types public.

We also need to slightly modify the test of `count qualified students` to add type annotation:

```moonbit
test "count qualified students" {
  let students: Array[@examine.Student] = [
    { id: "0", score: 10.0 },
    { id: "1", score: 50.0 },
    { id: "2", score: 61.0 },
  ]
  let criteria1 = fn(student) { @examine.is_qualified(student, 10) }
  let criteria2 = fn(student) { @examine.is_qualified(student, 50) }
  assert_eq(@examine.count_qualified_students(students, criteria1), 3)
  assert_eq(@examine.count_qualified_students(students, criteria2), 2)
}
```

Note that we access the type and the functions with `@examine`, the name of your package. This is how others use your package, but you can omit them in the black box tests.

And now, the compilation should work and the tests should pass again.

#### Publishing the library

Now that you're ready, you can publish this project to [mooncakes.io](https://mooncakes.io),
the module registry of MoonBit. You can find other interesting projects there
too.

1. Execute `moon login` and follow the instruction to create your account with
   an existing GitHub account.
2. Modify the project name in `moon.mod.json` to
   `<your github account name>/<project name>`. Run `moon check` to see if
   there's any other affected places in `moon.pkg`.
3. Execute `moon publish` and your done. Your project will be available for
   others to use.

By default, the project will be shared under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0.html),
which is a permissive license allowing everyone to use. You can also use other licenses, such as the [MulanPSL 2.0](https://spdx.org/licenses/MulanPSL-2.0.html),
by changing the field `license` in `moon.mod.json` and the content of `LICENSE`.

#### Closing

At this point, we've learned about the very basic and most not-so-trivial
features of MoonBit, yet MoonBit is a feature-rich, multi-paradigm programming
language. Visit [language tours](https://tour.moonbitlang.com) for more information in grammar and basic types,
and other documents to get a better hold of MoonBit.

<!-- path: tutorial/cli-quickstart.md -->
## Native CLI Quickstart

This quickstart shows a simple but proper MoonBit CLI layout:

- keep argument parsing and business logic in a library package
- keep `cmd/main` small
- use `moonbitlang/async` for native IO
- test the pure parts without touching the network

This example uses `moonbitlang/async`, which currently supports the native backend best.

### Create the project

Start with a normal MoonBit module:

```bash
moon new download_cli
cd download_cli
moon add moonbitlang/async@0.19.2
```

`argparse` is already part of the standard library, so this quickstart only adds `moonbitlang/async`.

Set the preferred target to native in `moon.mod.json` so `moon run` and `moon build` default to the backend that `moonbitlang/async` supports best:

```json
{
  "name": "username/download_cli",
  "version": "0.1.0",
  "deps": {
    "moonbitlang/async": "0.19.2"
  },
  "preferred-target": "native"
}
```

The final layout will look like this:

```text
download_cli
├── cmd
│   └── main
│       ├── main.mbt
│       └── moon.pkg
├── cli_test.mbt
├── config.mbt
├── download.mbt
├── moon.mod.json
└── moon.pkg
```

### Keep parsing in the library package

The root package defines the CLI contract. It owns the command shape and turns argv into a typed config value:

```moonbit
pub struct Config {
  url : String
  output : String?
} derive(Eq)

///|
pub fn command() -> @argparse.Command {
  @argparse.Command(
    "moon-fetch",
    about="Download a URL to stdout or a file",
    options=[
      @argparse.OptionArg(
        "output",
        short='o',
        about="Write the response body to this file",
      ),
    ],
    positionals=[
      @argparse.PositionArg(
        "url",
        about="HTTP or HTTPS URL to download",
        num_args=@argparse.ValueRange::single(),
      ),
    ],
  )
}

///|
pub fn parse_config(argv : ArrayView[String]) -> Config raise {
  let matches = @argparse.parse(command(), argv~)
  let values : Map[String, Array[String]] = matches.values
  guard values is { "url": [url], "output"? : output_paths, .. } else {
    fail("missing url")
  }
  let output = match output_paths {
    Some([output, ..]) => Some(output)
    _ => None
  }
  { url, output }
}
```

The package descriptor imports `argparse` from `moonbitlang/core` and the async libraries used by the implementation:

```moonbit
import {
  "moonbitlang/core/argparse",
  "moonbitlang/core/test",
  "moonbitlang/async/fs",
  "moonbitlang/async/http",
  "moonbitlang/async/stdio",
}
```

### Put async IO behind one function

`run` performs the actual download. If `-o` is passed, it streams the body into a file. Otherwise it writes directly to stdout:

```moonbit
pub async fn run(config : Config) -> Unit {
  let (response, body) = @http.get_stream(config.url)
  defer body.close()

  guard response.code is (200..<300) else {
    fail("download failed: \{response.code} \{response.reason}")
  }

  match config.output {
    Some(path) => {
      let file = @fs.create(path, permission=0o644)
      defer file.close()
      file.write_reader(body)
      @stdio.stderr.write("saved \{config.url} to \{path}\n")
    }
    None => @stdio.stdout.write_reader(body)
  }
}
```

### Keep `main` thin

`cmd/main` should usually do only wiring: read argv, build config, and call the library entrypoint.

```moonbit
import {
  "moonbit-community/cli-quickstart-doc" @app,
  "moonbitlang/core/env",
  "moonbitlang/async",
}

options(
  "is-main": true,
)
```

```moonbit
async fn main {
  let argv = @env.args()
  let config = @app.parse_config(argv[1:])
  @app.run(config)
}
```

### Run the command

Write the response body to stdout:

```bash
moon run cmd/main https://example.com/feed.xml
```

Write it to a file:

```bash
moon run cmd/main https://example.com/feed.xml -o feed.xml
```

Build a native binary:

```bash
moon build --target native
```

### Test the pure part

The parser and config shaping logic stay easy to test because they do not perform IO:

```moonbit
///|
test "parse config for stdout" {
  let config = parse_config(["https://example.com/feed.xml"])
  assert_eq(config.url, "https://example.com/feed.xml")
  @test.assert_eq(config.output, None)
}

///|
test "parse config for file output" {
  let config = parse_config(["https://example.com/feed.xml", "-o", "feed.xml"])
  assert_eq(config.url, "https://example.com/feed.xml")
  guard config.output is Some(path) else { fail("expected an output path") }
  assert_eq(path, "feed.xml")
}
```

Run the tests with:

```bash
moon test
```

When the CLI grows, keep following the same split:

- parse and validate inputs in the library package
- keep side effects in a small number of async functions
- keep `cmd/main` focused on wiring

<!-- path: tutorial/fullstack-one-project.md -->
## Fullstack in One MoonBit Project

This tutorial builds a small fullstack app in one MoonBit module.

You will implement one shared validation rule set and use it in both places:

- `frontend/`: show local warnings and call backend
- `backend/`: validate again and return JSON response

The key is `supported-targets`:

- `frontend/` is `js`
- `backend/` is `native`
- `shared/` is target-agnostic

### Prerequisites

- MoonBit toolchain installed
- `hurl` installed for API testing

### Step 1: Create the module

```bash
moon new fullstack_one_project
cd fullstack_one_project
moon add moonbitlang/async@0.19.2
moon add moonbit-community/rabbita
```

Project layout:

```text
fullstack_one_project
├── Makefile
├── moon.mod.json
├── backend
│   ├── api.hurl
│   ├── index.html
│   ├── main.mbt
│   └── moon.pkg
├── frontend
│   ├── main.mbt
│   └── moon.pkg
└── shared
    ├── moon.pkg
    ├── shared_test.mbt
    └── task.mbt
```

Module config:

```json
{
  "name": "moonbit-community/fullstack-one-project-doc",
  "version": "0.1.0",
  "deps": {
    "moonbitlang/async": "0.19.2",
    "moonbit-community/rabbita": "0.11.5"
  },
  "preferred-target": "native",
  "supported-targets": "+wasm+wasm-gc+js+native"
}
```

### Step 2: Implement shared domain validation

Define request/response types with `derive(ToJson, FromJson)` and one `suberror`-based validator in `shared/`.
Both frontend and backend import this package.

```moonbit
import {
  "moonbitlang/core/json" @json,
}
```

```moonbit
pub(all) struct SubmitTitleRequest {
  title : String
} derive(Eq, ToJson, FromJson)

///|
pub(all) suberror TitleValidationError {
  EmptyTitle
  TooLong(Int)
  ForbiddenHash
} derive(Eq, ToJson, FromJson)

///|
pub(all) enum SubmitTitleResponse {
  Accepted(String)
  ValidationError(TitleValidationError)
  InvalidJson
} derive(Eq, ToJson, FromJson)

///|
pub fn validate_request(
  request : SubmitTitleRequest,
) -> Unit raise TitleValidationError {
  let title = request.title.trim().to_owned()
  if title.length() == 0 {
    raise EmptyTitle
  } else if title.length() > 24 {
    raise TooLong(title.length())
  } else if title.rev_find("#") is Some(_) {
    raise ForbiddenHash
  }
}

///|
pub fn warning_text(err : TitleValidationError) -> String {
  match err {
    EmptyTitle => "title cannot be empty"
    TooLong(length) => "title is too long (\{length}), max is 24"
    ForbiddenHash => "title cannot contain '#'"
  }
}

///|
pub impl Show for SubmitTitleResponse with output(self, logger) {
  let text = match self {
    Accepted(title) => "accepted: \{title}"
    ValidationError(err) => "validation_error: \{warning_text(err)}"
    InvalidJson => "invalid_json: invalid request json"
  }
  logger.write_string(text)
}
```

### Step 3: Implement the frontend (`js`)

Frontend behavior:

- validate title locally with shared rules
- if valid, `POST` to backend `/submit`
- display backend response text

```moonbit
import {
  "moonbit-community/fullstack-one-project-doc/shared" @shared,
  "moonbitlang/core/json" @json,
  "moonbit-community/rabbita" @rabbita,
  "moonbit-community/rabbita/html" @html,
  "moonbit-community/rabbita/http" @rhttp,
}

supported_targets = "js"

options(
  "is-main": true,
)
```

```moonbit
fn main {
  let app = @rabbita.cell(
    model={ title: "", warning: None, server_message: None },
    update=(dispatch, msg, model) => {
      match msg {
        Edit(title) => {
          let warning = local_warning(title)
          (@rabbita.none, { title, warning, server_message: None })
        }
        Submit =>
          match model.warning {
            Some(message) =>
              (
                @rabbita.none,
                { ..model, server_message: Some("not sent: \{message}") },
              )
            None => {
              let request = @shared.SubmitTitleRequest::{ title: model.title }
              let request_json = request.to_json().stringify()
              let expect : @rhttp.Expecting[@rabbita.Cmd, Unit] = @rhttp.Expecting::Text(result => {
                  dispatch(ServerReplied(result))
                },
              )
              let cmd = @rhttp.post(
                "http://127.0.0.1:8080/submit",
                @rhttp.Body::Text(request_json),
                expect~,
              )
              (
                cmd,
                { ..model, server_message: Some("sending json request...") },
              )
            }
          }
        ServerReplied(result) => {
          let server_message = match result {
            Ok(raw_json) =>
              try {
                let response : @shared.SubmitTitleResponse = @json.from_json(
                  @json.parse(raw_json),
                )
                Some("\{response}")
              } catch {
                _ => Some("invalid backend response json")
              }
            Err(err) => Some("request failed: \{err}")
          }
          (@rabbita.none, { ..model, server_message, })
        }
      }
    },
    view=(dispatch, model) => {
      let warning_line = match model.warning {
        Some(message) => p("warning: \{message}")
        None => p("local validation passed")
      }
      let server_line = match model.server_message {
        Some(response) => p("backend response: \{response}")
        None => p("backend response: (none yet)")
      }
      let value = model.title
      div([
        h2("Shared Validation Demo"),
        input(
          input_type=Text,
          value~,
          on_input=text => dispatch(Edit(text)),
          nothing,
        ),
        button(on_click=dispatch(Submit), "Submit as JSON"),
        warning_line,
        server_line,
      ])
    },
  )
  @rabbita.new(app).mount("app")
}
```

### Step 4: Implement the backend (`native`)

Backend behavior:

- serve `GET /` from static `backend/index.html`
- serve `GET /frontend.js` from frontend build output
- handle `POST /submit` with shared validation and JSON response

```moonbit
import {
  "moonbit-community/fullstack-one-project-doc/shared" @shared,
  "moonbitlang/core/json" @json,
  "moonbitlang/async",
  "moonbitlang/async/fs" @fs,
  "moonbitlang/async/http" @http,
  "moonbitlang/async/socket" @socket,
  "moonbitlang/async/stdio",
}

supported_targets = "native"

options(
  "is-main": true,
)
```

```html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Shared Validation Demo</title>
  </head>
  <body>
    <h1>Shared Validation Demo</h1>
    <p>Backend serves this page and the built frontend bundle.</p>
    <div id="app"></div>
    <script src="/frontend.js"></script>
  </body>
</html>
```

```moonbit
async fn main {
  @stdio.stdout.write("starting backend on http://127.0.0.1:8080\n")
  let server = @http.Server(@socket.Addr::parse("127.0.0.1:8080")) catch {
    err => {
      @stdio.stdout.write("failed to start backend: \{err}\n")
      return
    }
  }

  server.run_forever((request, body, conn) => {
    match (request.meth, request.path) {
      (Get, "/") =>
        send_file(
          conn, index_html_path, html_headers, "missing backend/index.html",
        )
      (Get, "/frontend.js") =>
        send_file(
          conn, frontend_js_path, js_headers, "missing frontend bundle; run `moon build frontend --target js`",
        )
      (Post, "/submit") => {
        let raw_body = body.read_all().text() catch { _ => "" }
        let response : @shared.SubmitTitleResponse = try {
          let request : @shared.SubmitTitleRequest = @json.from_json(
            @json.parse(raw_body),
          )
          try {
            @shared.validate_request(request)
            @shared.SubmitTitleResponse::Accepted(
              request.title.trim().to_owned(),
            )
          } catch {
            err => @shared.SubmitTitleResponse::ValidationError(err)
          }
        } catch {
          _ => @shared.SubmitTitleResponse::InvalidJson
        }
        let code = match response {
          @shared.SubmitTitleResponse::Accepted(_) => 200
          _ => 400
        }
        let reason = if code == 200 { "OK" } else { "BadRequest" }
        conn
        ..send_response(code, reason, extra_headers=json_headers)
        ..write(response.to_json().stringify())
        .end_response()
      }
      _ =>
        conn
        ..send_response(404, "NotFound", extra_headers=text_headers)
        ..write("Not Found")
        .end_response()
    }
  })
}
```

### Step 5: Use Makefile shortcuts

```makefile
.PHONY: help build-frontend run-backend check test api-test verify verify-all clean

help:
	@echo "Targets:"
	@echo "  make build-frontend  Build frontend JS bundle"
	@echo "  make run-backend     Run backend server on 127.0.0.1:8080"
	@echo "  make check           Run moon check for all targets"
	@echo "  make test            Run moon test for all targets"
	@echo "  make api-test        Run Hurl API tests against local backend"
	@echo "  make verify          Run check + test"
	@echo "  make verify-all      Run verify + api-test"
	@echo "  make clean           Remove build artifacts"

build-frontend:
	moon build frontend --target js

run-backend:
	moon run backend --target native

check:
	moon check --deny-warn --target all

test:
	moon test --deny-warn --target all

api-test: build-frontend
	@command -v hurl >/dev/null 2>&1 || { echo "hurl is required for api-test"; exit 1; }
	@set -eu; \
		moon run backend --target native >/tmp/fullstack-one-project-backend.log 2>&1 & \
		pid=$$!; \
		trap 'kill $$pid >/dev/null 2>&1 || true' EXIT INT TERM; \
		sleep 1; \
		hurl --test backend/api.hurl

verify: check test

verify-all: verify api-test

clean:
	rm -rf _build
```

Common workflow:

```bash
make build-frontend
make run-backend
```

Then open `http://127.0.0.1:8080/` in a browser.

### Step 6: Test API with Hurl

Hurl test suite:

```hurl
GET http://127.0.0.1:8080/
HTTP 200
[Asserts]
body contains "<div id=\"app\"></div>"

GET http://127.0.0.1:8080/frontend.js
HTTP 200
[Asserts]
body contains "function"

POST http://127.0.0.1:8080/submit
Content-Type: application/json
{
  "title": "Write docs"
}
HTTP 200
[Asserts]
jsonpath "$[0]" == "Accepted"
jsonpath "$[1]" == "Write docs"

POST http://127.0.0.1:8080/submit
Content-Type: application/json
{
  "title": "bad #title"
}
HTTP 400
[Asserts]
jsonpath "$[0]" == "ValidationError"
jsonpath "$[1]" == "ForbiddenHash"

POST http://127.0.0.1:8080/submit
Content-Type: application/json
{
  "title": "01234567890123456789012345"
}
HTTP 400
[Asserts]
jsonpath "$[0]" == "ValidationError"
jsonpath "$[1][0]" == "TooLong"
jsonpath "$[1][1]" == 26

POST http://127.0.0.1:8080/submit
Content-Type: application/json
```
{"title":
```
HTTP 400
[Asserts]
jsonpath "$" == "InvalidJson"
```

Run it:

```bash
make api-test
```

This verifies:

- static `GET /` and `GET /frontend.js`
- accepted submit (`200`)
- rejected submit (`400`) for invalid titles
- rejected submit (`400`) for malformed JSON input

### Step 7: Verify everything

```bash
make verify-all
```

This runs:

- `moon check --deny-warn --target all`
- `moon test --deny-warn --target all`
- Hurl API tests

You now have one executable project where frontend and backend share the same validation contract and error model.
