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:
-
Node.js (version 16 or later) — download from nodejs.org. Check your version with
node --version. - 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"
)
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)
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
- Railway — deploy from Git, free tier available
- Fly.io — deploy with a Dockerfile, generous free tier
- Render — simple web service deployment
- A VPS (DigitalOcean, Linode, Vultr) — full control, starts around $5/month
- Vercel — serverless functions (compile with
quill buildfirst)
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")
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
- Read the standard library reference for the
createServerhelper. - Explore the Express documentation for the full API (middleware, error handling, templating).
- Check out the Examples page for more Quill programs.
- Join the Quill Discord community to share your project and get help.