Building Web Servers with Quill

Quill makes it easy to build web servers and REST APIs with readable syntax. Under the hood, Quill compiles to JavaScript and uses Express — so you get a battle-tested HTTP framework with Quill's English-like code style.

Prerequisites

Before you start, make sure you have:

  1. Node.js (version 16 or later) — download from nodejs.org. Check your version with node --version.
  2. Quill installed — see the Getting Started guide.

Quick Start

The fastest way to get a web server running is with the built-in scaffolding command:

quill web my-api
cd my-api

This creates a new project with the following structure:

my-api/
  server.quill       -- Your server code
  package.json       -- Node.js dependencies (express)
  .env               -- Configuration (port, etc.)
  .gitignore         -- Keeps .env and build output out of version control

Step 1: Install dependencies:

npm install

Step 2: Run the server:

quill run server.quill

Your server should start on http://localhost:3000. Open it in your browser to see "Hello from Quill!".

Or build and run with Node directly:

quill build server.quill
node server.js

Routes

Quill uses the on keyword with an HTTP method to define routes. The syntax is clean and readable:

use "express" as express

app is createServer()

-- GET request
app on get "/" with req res:
  res.send("Hello from Quill!")

-- POST request
app on post "/api/items" with req res:
  item is req.body
  res.json({created: item})

-- PUT request
app on put "/api/items/:id" with req res:
  id is req.params.id
  res.json({updated: id})

-- DELETE request
app on delete "/api/items/:id" with req res:
  id is req.params.id
  res.json({deleted: id})

app.listen(3000)

This compiles to standard Express code:

app.get("/", (req, res) => { ... });
app.post("/api/items", (req, res) => { ... });
app.put("/api/items/:id", (req, res) => { ... });
app.delete("/api/items/:id", (req, res) => { ... });

URL parameters

Use :param syntax in route paths to capture URL parameters. Access them via req.params:

app on get "/users/:id" with req res:
  userId is req.params.id
  res.json({id: userId})

Query parameters

Access query string parameters via req.query:

-- GET /search?q=quill&limit=10
app on get "/search" with req res:
  query is req.query.q
  limit is req.query.limit
  res.json({query: query, limit: limit})

JSON APIs

The createServer() helper automatically configures JSON body parsing so you can build APIs right away.

use "express" as express

app is createServer()

-- In-memory store
todos are []

-- List all todos
app on get "/api/todos" with req res:
  res.json(todos)

-- Create a todo
app on post "/api/todos" with req res:
  todo is req.body
  todos.push(todo)
  res.status(201).json(todo)

-- Status codes
app on get "/api/health" with req res:
  res.status(200).json({status: "healthy"})

app.listen(3000, with:
  say "API running at http://localhost:3000"
)
Tip: The createServer() helper creates an Express app with express.json() and express.urlencoded() middleware pre-configured. If you need more control, create the app manually with express().

Manual setup

If you prefer to configure Express yourself:

use "express" as express

app is express()
app.use(express.json())

app on get "/" with req res:
  res.send("Hello!")

app.listen(3000)

Middleware

Express middleware can be added using the standard app.use() call syntax:

use "express" as express
use "cors" as cors

app is createServer()

-- Enable CORS
app.use(cors())

-- Custom logging middleware
app.use(with req res next:
  say "{req.method} {req.url}"
)

-- Your routes here...
app on get "/" with req res:
  res.send("Hello!")

app.listen(3000)
Note: For inline middleware with the with callback syntax, the callback takes a single expression. For multi-line middleware, define it as a function and pass it to app.use().

Static Files

Serve static files (HTML, CSS, images) from a directory:

use "express" as express

-- Option 1: Use the createServer helper
app is createServer({static: "public"})

-- Option 2: Manual setup
app is express()
app.use(express.static("public"))

app.listen(3000)

Place your files in a public/ directory:

my-api/
  public/
    index.html
    style.css
    logo.png
  server.quill
  package.json

Full Example: REST API

Here is a complete REST API with CRUD routes:

use "express" as express

app is createServer()

-- In-memory data store
items are []
nextId is 1

-- List all items
app on get "/api/items" with req res:
  res.json(items)

-- Get a single item
app on get "/api/items/:id" with req res:
  id is Number(req.params.id)
  item is items.find(with i: i.id is id)
  if item:
    res.json(item)
  otherwise:
    res.status(404).json({error: "Not found"})

-- Create an item
app on post "/api/items" with req res:
  item is {id: nextId, name: req.body.name}
  nextId is nextId + 1
  items.push(item)
  res.status(201).json(item)

-- Delete an item
app on delete "/api/items/:id" with req res:
  id is Number(req.params.id)
  items is items.filter(with i: i.id is not id)
  res.json({deleted: id})

-- Start the server
port is process.env.PORT
if port is nothing:
  port is 3000

app.listen(port, with:
  say "API running at http://localhost:{port}"
)

Deployment

Once your server is working locally, deploy it to production.

Build for production

quill build server.quill

This produces a server.js file that runs on any system with Node.js.

Use environment variables

Never hardcode secrets or configuration. Use environment variables:

-- Read the port from the environment
port is process.env.PORT
if port is nothing:
  port is 3000

app.listen(port)

Keep the server running

Use a process manager like pm2:

# Install pm2
npm install -g pm2

# Start the server
pm2 start server.js --name my-api

# View logs
pm2 logs my-api

# Restart after updates
quill build server.quill
pm2 restart my-api

Hosting options

Using Docker

FROM node:20-slim
WORKDIR /app
COPY package.json .
RUN npm install --production
COPY server.js .
EXPOSE 3000
CMD ["node", "server.js"]

Build and run:

docker build -t my-api .
docker run -d -p 3000:3000 --env-file .env my-api

Declarative server: Block

In addition to the Express-style app on get syntax, Quill supports a declarative server: block for defining your entire server in a single, readable structure. This is ideal for smaller APIs or when you want a clean overview of all your routes.

server:
  port is 3000

  get "/" with req res:
    res.send("Hello from Quill!")

  get "/api/users" with req res:
    res.json([{name: "Alice"}, {name: "Bob"}])

  post "/api/users" with req res:
    user is req.body
    res.status(201).json({created: user})

  delete "/api/users/:id" with req res:
    res.json({deleted: req.params.id})

The server: block automatically creates an Express app, configures JSON body parsing, and starts listening on the specified port. You do not need to call createServer() or app.listen() yourself.

Adding middleware

Use the use keyword inside the block to attach middleware:

use "cors" as cors

server:
  port is 3000

  use cors()

  get "/" with req res:
    res.send("Hello!")

Static files in a server block

server:
  port is 3000
  static "public"

  get "/" with req res:
    res.send("Home page")
Tip: The server: block and the app on get style are interchangeable. Use whichever reads better for your project. The server: block is great for quick prototypes; the manual style gives you full control over Express configuration.

Next Steps