Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: MoonBit CI

on:
push:
branches: [ master, main, prod-server-ready ]
pull_request:
branches: [ master, main ]

jobs:
check-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install MoonBit
run: |
curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash
echo "$HOME/.moon/bin" >> $GITHUB_PATH

- name: MoonBit Update
run: moon update

- name: MoonBit Check
run: moon check --target native

- name: MoonBit Build
run: moon build --target native

docker-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker Image
run: docker build -t moon-web-relay:test .
84 changes: 44 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,67 +1,71 @@
# MoonBit Webserver with Crescent & Relay
# 🏮 MoonBit Production-Ready Webserver Template

This project is a high-performance, native webserver built with **MoonBit**. It leverages **Crescent** for routing and **Relay** for asynchronous background task processing.
A professional, high-performance webserver template built with **MoonBit**, leveraging **Crescent** for native routing and **Relay** for asynchronous background processing.

## 🏗 Architecture
## 🏗 High-Level Architecture

The application is structured into a modern, native-compiled stack:
This template provides a modular foundation for building scalable services:

- **Core**: Written in [MoonBit](https://www.moonbitlang.com/), targeting **Native** for maximum performance.
- **HTTP Layer**: Powered by [Crescent v0.9.0](https://mooncakes.io/docs/bobzhang/crescent). It provides a robust routing system, group middleware, and ergonomic response handlers.
- **Message Queue**: Integrated with [Relay v0.1.0](https://mooncakes.io/docs/Metalymph/relay).
- Supports asynchronous job processing.
- Opt-in background workers for heavy tasks.
- Defaulting to an efficient `InMemoryBackend`.
- **Infrastructure**: Distributed via a multi-stage **Docker** build that produces a minimal standalone binary (~1-2MB) running on `debian:slim`.
- **`cmd/main/`**: The application entry point. Defines routes, attaches middleware, and coordinates the web server and background workers using structured concurrency (`with_task_group`).
- **`lib/config/`**: Centralized configuration management. Uses `@string.parse_int` for type-safe environment variable parsing.
- **`lib/logger/`**: Structured JSON logging with level-based filtering (`DEBUG`, `INFO`, `ERROR`). Optimized for modern observability stacks.
- **Relay Processing**: Pluggable background task system. Supports **InMemory** for local development and **Valkey/Redis** for persistent production workloads.

## 🚀 Getting Started

### Prerequisites

- [MoonBit](https://www.moonbitlang.com/install/) toolchain installed.
- [watchexec](https://github.com/watchexec/watchexec) (optional, for hot-reload).
- [just](https://github.com/casey/just) or `make` for command execution.
- [MoonBit](https://www.moonbitlang.com/install/) toolchain.
- [Docker](https://www.docker.com/) & [Docker Compose](https://docs.docker.com/compose/).

### Development
### Development (Local)

Run the server with the **Relay** architecture enabled or disabled:
Run with standard settings (InMemory Relay, Info logs):

```bash
# Default: Relay disabled
just dev

# Opt-in: Relay enabled (launches background workers)
just relay=true dev
```

Or using `make`:
### Production Preview (Valkey)

Start the full stack with persistent messaging and debug logs enabled:

```bash
make dev RELAY=true
LOG_LEVEL=DEBUG docker compose up --build
```

### Features & Endpoints
## 📂 Design Implementation Details

- `GET /`: Basic heartbeat.
- `GET /hello/:name`: Dynamic routing.
- `GET /json`: High-performance JSON serialization.
- `GET /relay/push?payload=...`: (Only if Relay is enabled) Pushes a task to the background worker.
### Configuration Logic

## 🐳 Docker Deployment
The `Config` struct in `lib/config` automatically resolves overrides from the environment. This ensures that the same binary can be deployed across multiple environments (staging, production) without modification.

The project uses a serious multi-stage Dockerfile that builds the system from source and exports only the binary.
### Structured Logging

```bash
# Build the image
just docker-build
Logs are emitted as JSON objects to standard output. This allows for easy parsing by agents like FluentBit or Datadog. The `LOG_LEVEL` toggle prevents noise in production while allowing deep visibility during debugging.

# Run the container
just relay=true docker-run
```
### Multi-Backend Relay

The server can be configured to use `Valkey` as a message backend. If the connection fails, the system gracefully fails back to an `InMemory` queue to prevent data loss or server crashes, while logging the incident as an `ERROR`.

## 🛠 environment Configuration

| Variable | Default | Description |
| :--- | :--- | :--- |
| `PORT` | `4000` | Server listening port |
| `USE_RELAY` | `false` | Enable/Disable Relay workers |
| `RELAY_BACKEND` | `memory` | `memory` or `valkey` |
| `VALKEY_URL` | `valkey://localhost:6379` | Connection string for Valkey |
| `LOG_LEVEL` | `INFO` | `DEBUG`, `INFO`, `ERROR` |

## 🐳 Deployment

The multi-stage `Dockerfile` produces a minimal (~2MB) native binary image based on `debian:slim`. It leverages the modern MoonBit build system to produce zero-dependency, high-performance artifacts.

## 🤖 CI/CD Foundations

## 🛠 Project Structure
Included in `.github/workflows/ci.yml` is an automated pipeline that:

- `cmd/main/main.mbt`: Application entry point and route definitions.
- `moon.mod.json`: Project dependencies and metadata.
- `justfile` / `Makefile`: Ergonomic shortcuts for common tasks.
- `Dockerfile`: Production-ready multi-stage container build.
1. Performs static analysis (`moon check`).
2. Runs the test suite (`moon test`).
3. Validates the container build.
178 changes: 75 additions & 103 deletions cmd/main/main.mbt
Original file line number Diff line number Diff line change
@@ -1,17 +1,51 @@
// --- Webserver Template ---

///|
async fn main {
// 1. Load Configuration
let cfg = @config.Config::load()
@logger.info("🚀 Starting server on port \{cfg.port}")

@async.with_task_group(
async fn(tg) {
let app = @crescent.Mocket()

// --- Relay Setup (Opt-in) ---
let use_relay = @sys.get_env_var("USE_RELAY").unwrap_or("false") == "true"
if use_relay {
println("🏮 Relay: Initializing background workers...")
let backend : @relay.InMemoryBackend[String] = @relay.InMemoryBackend::new(
1000,
)
let queue = backend.to_relay_queue()
// 2. Relay Setup (Opt-in & Multi-backend)
if cfg.relay_enabled {
@logger.info("🏮 Relay: Initializing background workers...")

let queue = match cfg.relay_backend {
InMemory => {
@logger.info("📦 Using InMemory Relay backend")
let backend : @relay.InMemoryBackend[String] = @relay.InMemoryBackend::new(
1000,
)
backend.to_relay_queue()
}
Valkey => {
@logger.info("🔌 Connecting to Valkey (localhost:6379)")
try {
// In production, you would parse cfg.valkey_url to get host/port
let valkey_client = @valkey.Client::connect("localhost", 6379)
let backend = @relay.RedisBackend::new(
valkey_client,
"relay-tasks",
fn(s) { s },
fn(s) { s },
)
backend.to_relay_queue()
} catch {
err => {
@logger.error("❌ Failed to connect to Valkey: \{err}")
@logger.info("⚠️ Falling back to InMemory Relay")
let backend : @relay.InMemoryBackend[String] = @relay.InMemoryBackend::new(
1000,
)
backend.to_relay_queue()
}
}
}
}

// Example worker: Process messages from the queue
tg.spawn_bg(
Expand All @@ -20,144 +54,82 @@ async fn main {
@relay.start_worker(
queue,
fn(msg) {
println(
@logger.info(
"👷 Relay Worker: Processing message \{msg.id} -> \{msg.payload}",
)
},
)
} catch {
_ => println("🛑 Relay Worker stopped.")
_ => @logger.info("🛑 Relay Worker stopped.")
}
},
)

// Add endpoint to push tasks to relay
// Endpoint to push tasks to relay
app.get(
"/relay/push",
async fn(event) {
let payload = event.req.get_query("payload").unwrap_or("default-task")
try {
queue.push(payload)
@logger.info("📥 Task queued: \{payload}")
} catch {
err => println("❌ Relay Push Error: \{err}")
err => @logger.error("❌ Relay Push Error: \{err}")
}
"Task pushed to Relay: \{payload}"
"Task queued: \{payload}"
},
)
}

// Register global middleware
// 3. Middlewares
app
..use_middleware(
(event, next) => {
println("📝 Request: \{event.req.http_method} \{event.req.url}")
@logger.debug("📝 Request: \{event.req.http_method} \{event.req.url}")
next()
},
)

// Text Response
..get("/", _event => "⚡️ Tadaa!")

// Hello World
..on("GET", "/hello", _ => "Hello world!")
.group(
"/api",
(group) => {
// 添加组级中间件
group.use_middleware(
(event, next) => {
println(
"🔒 API Group Middleware: \{event.req.http_method} \{event.req.url}",
)
next()
},
)
group.get("/hello", _ => "Hello world!")
group.get(
"/json",
_ => ({ "name": "John Doe", "age": 30, "city": "New York" } : Json),
)
// Example of adding security headers (custom middleware)
..use_middleware(
(event, next) => {
ignore(event.res.header("X-Content-Type-Options", "nosniff"))
ignore(event.res.header("X-Frame-Options", "DENY"))
next()
},
)

// JSON Response
app
..get(
"/json",
_event => ({ "name": "John Doe", "age": 30, "city": "New York" } : Json),
)

// Async Response
..get(
"/async_data",
_event => ({ "name": "John Doe", "age": 30, "city": "New York" } : Json),
)

// Dynamic Routes
// /hello2/World = Hello, World!
..get(
"/hello/:name",
(event) => {
let name = event.param("name").unwrap_or("World")
"Hello, \{name}!"
},
)
// /hello2/World = Hello, World!
..get(
"/hello2/*",
(event) => {
let name = event.param("_").unwrap_or("World")
"Hello, \{name}!"
},
)
// 4. Routes
// Basic heartbeat
..get("/", _event => "⚡️ Welcome to the Production-Ready MoonBit Server!")

// Wildcard Routes
// /hello3/World/World = Hello, World/World!
// Health checks (Phase 3)
..get(
"/hello3/**",
(event) => {
let name = event.param("_").unwrap_or("World")
"Hello, \{name}!"
},
"/healthz",
_event => ({ "status": "ok", "version": "1.0.0" } : Json),
)

// Echo Server
..post(
"/echo",
(e) => {
let body : Bytes = e.req.body()
body
},
)
// Hello World
..get("/hello/:name", (event) => {
let name = event.param("name").unwrap_or("World")
"Hello, \{name}!"
})

// 404 Page
// JSON example
.get(
"/404",
(e) => {
e.res.status_code = @crescent.StatusCode::from_int(404)
@crescent.html(
(
#|<html>
#|<body>
#| <h1>404</h1>
#|</body>
#|</html>
),
)
},
"/api/user",
_ => ({ "id": 1, "username": "moon_dev", "role": "admin" } : Json),
)

// Print Server URL
// 5. Server Startup
@logger.info("📡 Routes initialized:")
for path in app.routes() {
println("\{path.0} http://localhost:4000\{path.1}")
@logger.info(" \{path.0} -> \{path.1}")
}

// Serve
println("🚀 Server active on http://localhost:4000")
try {
app.serve(port=4000)
app.serve(port=cfg.port)
} catch {
err => println("Server error: \{err}")
err => @logger.error("🔥 Server Panic: \{err}")
}
},
)
Expand Down
5 changes: 4 additions & 1 deletion cmd/main/moon.pkg
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import {
"bobzhang/crescent" @crescent,
"bobzhang/crescent/core",
"moonbitlang/core/json",
"moonbitlang/async",
"moonbitlang/x/sys" @sys,
"Metalymph/relay" @relay,
"Metalymph/valkey" @valkey,
"username/webserver_example/lib/config" @config,
"username/webserver_example/lib/logger" @logger,
}

options(
Expand Down
Loading
Loading