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

以下是一些可能帮助您学习编程语言的教程：

## 通用教程

- [语言基础知识互动之旅](https://tour.moonbitlang.com)
- [新手之旅](tour.md)
- [原生 CLI 快速开始](cli-quickstart.md)
- [在一个 MoonBit 项目中开发全栈应用](fullstack-one-project.md)

## 语言迁移指南

- [MoonBit：Go 开发者入门指南](for-go-programmers/index.md)

<!-- path: tutorial/tour.md -->
## MoonBit：新手之旅

本指南面向初学者，且并不打算作为一个几分钟就能读完的小文章。本文旨在为那些对 MoonBit 的编程思路 （更加现代化，函数式的）不甚了解的用户提供一个简洁而不失易懂性的指南。

如果您想直接深入了解语言，请参阅 [总体介绍](../language/index.md)。

### 安装

**语言扩展**

目前，MoonBit 的开发支持是通过 VS Code 扩展实现的。请前往 [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=moonbit.moonbit-lang) 下载 MoonBit 语言支持。

**工具链**

> （推荐）如果您已安装了上面的扩展，运行操作菜单中的“Install moonbit toolchain” 即可直接安装运行时并跳过这部分介绍：![安装运行时](imgs/runtime-installation.png)

我们还提供了一个安装脚本：Linux 和 macOS 用户可以通过以下方式安装：

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

对于 Windows 用户，使用 Powershell：

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

这将自动安装 MoonBit 到 `$HOME/.moon` 并将其添加到您的 `PATH`。

如果安装后遇到 `moon` 未找到的情况，请尝试重新启动终端或 VSCode 以使环境变量生效。

请注意，目前 MoonBit 还不适用于生产环境：它正在积极开发中。要更新 MoonBit，只需再次运行上面的命令即可。

运行 `moon help` 可以看到一堆子命令。但是现在我们只需要 `build` `run` 和 `new` 这三个命令。

要创建一个项目（或者说模块），运行 `moon new <path>`，其中 <path> 是您希望放置项目的位置。例如，如果您执行 `moon new examine`，您将得到：

```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
```

其中包含一个 `cmd/main` 包，内有一个 `fn main` 作为程序的入口。尝试运行 `moon run cmd/main`。

在这个教程中，我们假设项目名称为 `examine`，当前工作目录也是 `examine`。

### 示例：找到通过考试的人

在这个例子中，我们将尝试找出，给定一些学生的分数，有多少人通过了考试？

为此，我们将从定义数据类型开始，确定我们的函数，并编写我们的测试。然后我们将实现我们的函数。

除非另有说明，以下内容将在文件 `examine.mbt` 中定义。

#### 数据类型

MoonBit 中的 [基本数据类型](../language/fundamentals.md#built-in-data-structures) 包括以下内容：

- `Unit`
- `Bool`
- `Int`, `UInt`, `Int64`, `UInt64`, `Byte`, ...
- `Float`, `Double`
- `Char`, `String`, ...
- `Array[T]`, ...
- 元组，和其他类型

要使用原始类型表示包含学生 ID 和分数的记录，我们可以使用一个包含学生 ID（类型为 `String`）和分数（类型为 `Double`）的 2-元组，如 `(String, Double)`。然而，这并不是很直观，因为我们无法区分其他可能的数据类型，例如包含学生 ID 和学生身高的记录。

因此，我们选择使用 [结构体](../language/fundamentals.md#struct) 声明我们自己的数据类型：

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

一个人可以通过或者不通过考试，因此判断结果可以使用 [枚举类型](../language/fundamentals.md#enum) 定义：

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

#### 函数

[函数](../language/fundamentals.md#functions) 是一段代码，它接受一些输入并产生一个结果。

在我们的例子中，我们需要判断一个学生是否通过了考试：

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

这个函数接受一个类型为 `Student` 的输入 `student`，一个类型为 `Double` 的输入 `criteria`（因为每门课程的标准可能不同，每个国家的标准可能不同）然后返回一个 `ExamResult`。

使用 `...` 语法可以让我们暂时不实现函数。

我们还需要找出有多少学生通过了考试：

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

在 MoonBit 中，函数是一等公民，这意味着我们可以将一个函数绑定到一个变量，将一个函数作为参数传递或将一个函数作为结果接收。这个函数接受一个学生记录数组和另一个函数，判断学生是否通过了考试。

#### 编写测试

我们可以定义内联测试来定义函数的预期行为。这也有助于确保在重构程序时不会出现回归（破坏现有行为）。

```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)
}
```

我们会收到报错信息，提醒我们 `ExamResult` 没有实现 `Show` 和 `Eq`。

`Show` 和 `Eq` 是 **traits**。在 MoonBit 中，trait 定义了一个类型应该能够执行的一些常见操作。

例如，`Eq` 定义了应该有一种方法来比较两个相同类型的值，这个方法叫做 `op_equal`：

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

`Show` 定义了应该有一种方法，要么将一个类型的值转换为 `String`，要么使用 `Logger` 写入：

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

`assert_eq` 使用它们来约束传递的参数，以便比较两个值并在它们不相等时打印它们：

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

我们需要为我们的 `ExamResult` 实现 `Eq` 和 `Show`。有两种方法可以实现。

1. 通过定义一个显式实现：
   ```moonbit
   impl Eq for ExamResult with equal(self, other) {
     match (self, other) {
       (Pass, Pass) | (Fail, Fail) => true
       _ => false
     }
   }
   ```

   在这里，我们使用 [模式匹配](../language/fundamentals.md#pattern-matching) 来检查 `ExamResult` 的情况。
2. 另一种方法是通过 [派生](../language/derive.md) ，因为 `Eq` 和 `Show` 是 [内置 traits](../language/methods.md#builtin-traits) 并且 `ExamResult` 的输出非常直接：
   ```moonbit
   enum ExamResult {
     Pass
     Fail
   } derive(Show)
   ```

现在我们已经实现了 traits，我们可以继续实现我们的测试：

```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)
}
```

在这里，我们使用 [lambda 表达式](../language/fundamentals.md#local-functions) 来重用先前定义的 `is_qualified` 来创建不同的标准。

我们可以运行 `moon test` 来查看测试是否成功。

#### 实现函数

对于 `is_qualified` 函数，只需要简单的比较：

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

在 MoonBit 中，最后一个表达式的结果是函数的返回值，每个分支的结果是 `if` 表达式的值。

对于 `count_qualified_students` 函数，我们需要遍历数组，检查每个学生是否通过。

一个简单的版本是使用一个可变值和一个 [`for` 循环](../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
}
```

然而，这种方式既不高效（因为有边界检查）也不够直观，因此我们可以用 [`for .. in` 循环](../language/fundamentals.md#for-in-loop) 替换普通的 `for` 循环：

```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
}
```

另一种方法是使用为 [迭代器](../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()
}
```

现在之前定义的测试应该通过了。

### 公开库

恭喜您完成了第一个 MoonBit 库！

您现在可以与其他开发人员分享它，这样他们就不需要重复您所做的工作。

但在此之前，您还有一些其他事情要做。

#### 调整可见性

为了看到其他人如何使用我们的程序，MoonBit 提供了一种称为 [“黑盒测试”](../language/tests.md#blackbox-tests-and-whitebox-tests) 的机制。

让我们将上面定义的 `test` 块移动到一个新文件 `top_test.mbt` 中。

糟糕！现在有错误抱怨：

- `is_qualified` 和 `count_qualified_students` 未绑定
- `Fail` 和 `Pass` 未定义
- `Student` 不是一个记录类型，字段 `id` 未找到，等等。

所有这些问题都来自于可见性问题。默认情况下，定义的函数对当前包（由当前文件夹绑定）之外的程序的其他部分不可见。默认情况下，类型被视为抽象类型，即我们只知道存在类型 `Student` 和类型 `ExamResult`。通过使用黑盒测试，您可以确保您希望其他人拥有的一切确实被赋予了预期的可见性。

为了让其他人使用这些函数，我们需要在 `fn` 前添加 `pub` 使函数公开。

为了让其他人构造类型和读取内容，我们需要在 `struct` 和 `enum` 前添加 `pub(all)` 使类型公开。

我们还需要稍微修改 `count qualified students` 的测试，添加类型注释：

```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)
}
```

请注意，我们使用 `@examine` 访问类型和函数，这是您的包的名称。这是其他人使用您的包的方式，但您可以在黑盒测试中省略它们。

现在，编译应该可以正常工作，测试应该再次通过。

#### 发布库

现在您已经准备好了，您可以将这个项目发布到 [mooncakes.io](https://mooncakes.io)，MoonBit 的模块注册中心。您也可以在那里找到其他有趣的项目。

1. 执行 `moon login` 并按照说明使用现有的 GitHub 账户创建您的账户。
2. 将 `moon.mod.json` 中的项目名修改为 `<你的 GitHub 账户名>/<项目名>`。运行 `moon check`，查看 `moon.pkg` 中是否还有其他受影响的位置。
3. 执行 `moon publish`，您就完成了。您的项目将可供他人使用。

默认情况下，项目将在 [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0.html) 下共享，这是一种宽松的许可证，允许每个人使用。您还可以通过更改 `moon.mod.json` 中的 `license` 字段和 `LICENSE` 的内容，使用其他许可证，例如 [MulanPSL 2.0](https://license.coscl.org.cn/MulanPSL2)。

#### 结束语

到目前为止，我们已经了解了 MoonBit 的基本特性和一些不那么简单的特性，然而 MoonBit 是一个功能丰富的、多范式的编程语言。访问 [语言导览](https://tour.moonbitlang.com) 了解更多语法和基本类型的信息，以及其他文档，更好地掌握 MoonBit。

<!-- path: tutorial/cli-quickstart.md -->
## 原生 CLI 快速开始

本快速开始展示了一个简单但规范的 MoonBit CLI 布局：

- 将参数解析和业务逻辑放在库包中
- 保持 `cmd/main` 简洁
- 使用 `moonbitlang/async` 处理原生 IO
- 在不接触网络的情况下测试纯逻辑部分

这个示例使用 `moonbitlang/async`，它目前对 native 后端支持最好。

### 创建项目

先从一个普通的 MoonBit 模块开始：

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

`argparse` 已经属于标准库，因此这个快速开始只需要添加 `moonbitlang/async`。

在 `moon.mod.json` 中把首选目标设置为 native，这样 `moon run` 和 `moon build` 默认就会使用 `moonbitlang/async` 支持最好的后端：

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

最终目录结构如下：

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

### 在库包中保留解析逻辑

根包定义 CLI 契约。它负责命令的形状，并把 argv 转成带类型的配置值：

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

///|
pub fn command() -> @argparse.Command {
  @argparse.Command::new(
    "moon-fetch",
    about="Download a URL to stdout or a file",
    options=[
      @argparse.OptionArg::new(
        "output",
        short='o',
        about="Write the response body to this file",
      ),
    ],
    positionals=[
      @argparse.PositionArg::new(
        "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 }
}
```

包描述文件从 `moonbitlang/core` 导入 `argparse`，并导入实现中使用的异步库：

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

### 把异步 IO 收拢到一个函数中

`run` 负责实际下载。传入 `-o` 时，它会把响应体写入文件；否则直接写到标准输出：

```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)
  }
}
```

### 保持 `main` 精简

`cmd/main` 通常只负责接线：读取 argv、构造配置并调用库入口点。

```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)
}
```

### 运行命令

将响应体写到标准输出：

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

将其写入文件：

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

构建原生二进制：

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

### 测试纯逻辑部分

解析器和配置整理逻辑不执行 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")
}
```

用下面的命令运行测试：

```bash
moon test
```

当 CLI 继续增长时，仍然沿用同样的拆分方式：

- 在库包中解析并校验输入
- 把副作用集中在少量 async 函数中
- 让 `cmd/main` 专注于接线

<!-- path: tutorial/fullstack-one-project.md -->
## 在一个 MoonBit 项目中开发全栈应用

本教程展示如何在一个 MoonBit 模块中构建一个小型全栈应用。

你将实现一套共享校验规则，并在以下两处复用：

- `frontend/`：展示本地告警并调用后端
- `backend/`：再次校验并返回 JSON 响应

关键在于 `supported-targets`：

- `frontend/` 为 `js`
- `backend/` 为 `native`
- `shared/` 与目标平台无关

### 前置条件

- 已安装 MoonBit 工具链
- 已安装用于 API 测试的 `hurl`

### 步骤 1：创建模块

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

项目结构：

```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
```

模块配置：

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

### 步骤 2：实现共享领域校验

在 `shared/` 中定义带 `derive(ToJson, FromJson)` 的请求/响应类型，以及一个基于 `suberror` 的校验器。前后端都导入这个包。

```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)
}
```

### 步骤 3：实现前端（`js`）

前端行为：

- 用共享规则在本地校验标题
- 若通过校验，则向后端 `/submit` 发送 `POST`
- 展示后端响应文本

```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")
}
```

### 步骤 4：实现后端（`native`）

后端行为：

- 将 `GET /` 映射到静态文件 `backend/index.html`
- 将 `GET /frontend.js` 映射到前端构建产物
- 用共享校验处理 `POST /submit` 并返回 JSON 响应

```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::new(@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()
    }
  })
}
```

### 步骤 5：使用 Makefile 快捷命令

```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
```

常用工作流：

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

然后在浏览器中打开 `http://127.0.0.1:8080/`。

### 步骤 6：用 Hurl 测试 API

Hurl 测试套件：

```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"
```

运行：

```bash
make api-test
```

这会验证：

- 静态 `GET /` 与 `GET /frontend.js`
- 合法提交（`200`）
- 标题非法时被拒绝提交（`400`）
- JSON 输入格式错误时被拒绝提交（`400`）

### 步骤 7：验证全部内容

```bash
make verify-all
```

会执行：

- `moon check --deny-warn --target all`
- `moon test --deny-warn --target all`
- Hurl API 测试

现在你已经有了一个可执行的项目：前后端共享同一套校验契约与错误模型。
