Skip to content

Quick Start

Build and run a REST API backed by a database in 10 minutes.

Prerequisites

  • Docker (recommended) or Go 1.21+
  • A terminal

Step 1: Create Your Service

mycel init my-first-service
cd my-first-service

That scaffolds a service that already runs, in the layout that stays maintainable as it grows — one declaration per file, grouped by kind. See Project Structure.

To follow along by hand instead, create the directory and these files yourself:

mkdir my-first-service
cd my-first-service

Why three files, and why .mycel

Mycel reads every .mycel file under the config directory, recursively, and merges them into one service. The extension is the only thing that matters — file and directory names are for your benefit, not the runtime's. These three could equally be one file.

Project Structure covers how to lay this out as a service grows.

config.mycel — service identity

service {
  name    = "my-first-api"
  version = "1.0.0"
}

connectors.mycel — data sources

connector "api" {
  type = "rest"
  port = 3000
}

connector "db" {
  type     = "database"
  driver   = "sqlite"
  database = "./data.db"
}

flows.mycel — data flows

flow "list_items" {
  from {
    connector = "api"
    operation = "GET /items"
  }
  to {
    connector = "db"
    target    = "items"
  }
}

flow "create_item" {
  from {
    connector = "api"
    operation = "POST /items"
  }
  to {
    connector = "db"
    target    = "INSERT items"
  }
}

flow "get_item" {
  from {
    connector = "api"
    operation = "GET /items/:id"
  }
  to {
    connector = "db"
    target    = "items WHERE id = :id"
  }
}

Step 2: Run Your Service

With Docker

docker run -v $(pwd):/etc/mycel -p 3000:3000 ghcr.io/matutetandil/mycel

From source (requires Go 1.21+)

go install github.com/matutetandil/mycel/v2/cmd/mycel@latest
mycel start

You should see:

  __  __                  _
 |  \/  |_   _  ___ ___  | |
 | |\/| | | | |/ __/ _ \ | |
 | |  | | |_| | (_|  __/ | |
 |_|  |_|\__, |\___\___| |_|
         |___/

 Declarative Microservice Framework

INFO  Starting service: my-first-api
INFO  Loaded 2 connectors
INFO  Registered 3 flows
INFO  REST server listening on :3000

Step 3: Test Your API

Open a new terminal:

# Create an item
curl -X POST http://localhost:3000/items \
  -H "Content-Type: application/json" \
  -d '{"name": "My first item", "description": "Created with Mycel!"}'

Response:

{"id":1,"name":"My first item","description":"Created with Mycel!"}

# List all items
curl http://localhost:3000/items

Response:

[{"id":1,"name":"My first item","description":"Created with Mycel!"}]

# Get a single item
curl http://localhost:3000/items/1

You just created a REST API with a database backend without writing any code.

Step 4: Add Data Transformation

Add automatic UUIDs and timestamps. Update flows.mycel:

flow "create_item" {
  from {
    connector = "api"
    operation = "POST /items"
  }

  transform {
    id          = "uuid()"
    name        = "input.name"
    description = "input.description"
    created_at  = "now()"
  }

  to {
    connector = "db"
    target    = "INSERT items"
  }
}

Two variables appear here that nothing declared. input is the data that arrived — for a REST source, the request body, path and query parameters, all flat. The field name on the left of each line is what gets written out. See Input and Output for the full picture.

Test it:

curl -X POST http://localhost:3000/items \
  -H "Content-Type: application/json" \
  -d '{"name": "With UUID", "description": "Auto-generated ID"}'

Response:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "With UUID",
  "description": "Auto-generated ID",
  "created_at": "2024-01-15T10:30:00Z"
}

Step 5: Add Input Validation

Create types.mycel:

type "item_input" {
  name = string({
    required   = true
    min_length = 1
    max_length = 100
  })
  description = string({
    required   = false
    max_length = 500
  })
}

Reference it in the flow:

flow "create_item" {
  from {
    connector = "api"
    operation = "POST /items"
  }

  validate {
    input = "item_input"
  }

  transform {
    id          = "uuid()"
    name        = "input.name"
    description = "default(input.description, '')"
    created_at  = "now()"
  }

  to {
    connector = "db"
    target    = "INSERT items"
  }
}

Invalid requests are now rejected:

curl -X POST http://localhost:3000/items \
  -H "Content-Type: application/json" \
  -d '{}'

Response:

{
  "error": "validation failed",
  "details": {"name": "required field missing"}
}

What's Next

Use a real database

connector "db" {
  type     = "database"
  driver   = "postgres"
  host     = env("DB_HOST", "localhost")
  port     = 5432
  database = "myapp"
  user     = env("DB_USER", "postgres")
  password = env("DB_PASSWORD", "")
}

Add environment variables

Create a .env file (never commit it):

DB_HOST=localhost
DB_USER=postgres
DB_PASSWORD=secret
MYCEL_LOG_LEVEL=debug

Mycel loads it automatically on startup.

Deploy with Docker

docker run \
  -v ./config:/etc/mycel \
  -e MYCEL_ENV=production \
  -e MYCEL_LOG_FORMAT=json \
  -e DB_HOST=db.example.com \
  -e DB_PASSWORD=secret \
  ghcr.io/matutetandil/mycel

Core Concepts Summary

Concept What it does
connector Connects to an external system (database, API, queue, cache)
flow Defines how data moves from a source to a target
transform Reshapes data with CEL expressions
type Validates data structure with schema constraints

Reference