Apexlang: Project Scaffolding & Code Generation
Generate code, docs, and more from a single source
How often do you start projects the same way? With the same boilerplate, same dependencies, same configuration, same everything? If you’re like me, you get your settings dialed in and reuse them in a base project template repeatedly. Eventually you might find yourself creating git repos or scripts to make this process buttery smooth.
Tools like yeoman
, degit
, and cargo generate
kept me happy for years. They add basic templating capabilities to the standard git clone
but they stop there. You’ll be hard pressed to find tools that go beyond setting up a directory structure.
That’s where Apexlang comes in. Apexlang is a code generation and templating tool suite. It started as an interface definition language (IDL) for WebAssembly called WIDL but was too useful to be hidden away. It now contains the base functionality of tools like degit
combined with the code generation capability of tools like protoc
and smithy
. And unlike tools like protoc
and smithy
, the code generators are written in TypeScript so they are much easier to write and maintain.
With Apexlang you get your boilerplate along with code, documentation, schemas, and more all generated automatically, continuously, and easily.
Check it out:
$ apex new git@github.com:apexlang/codegen.git -p templates/nodejs my-project
Cloning into '/var/folders/pg/h7s94pd90w54hcbjpkvm58240000gn/T/77f11564'...
remote: Enumerating objects: 256, done.
remote: Counting objects: 100% (256/256), done.
remote: Compressing objects: 100% (210/210), done.
remote: Total 256 (delta 54), reused 135 (delta 18), pack-reused 0
Receiving objects: 100% (256/256), 150.38 KiB | 6.27 MiB/s, done.
Resolving deltas: 100% (54/54), done.
? Please enter the project description ›
INFO Writing file my-project/package.json (mode:100644)
INFO Writing file my-project/apex.yaml (mode:100644)
INFO Writing file my-project/apex.axdl (mode:100644)
INFO Writing file my-project/.gitignore (mode:100644)
INFO Writing file my-project/tsconfig.json (mode:100644)
INFO Writing file my-project/.vscode/settings.json (mode:100644)
INFO Writing file my-project/.vscode/tasks.json (mode:100644
The apex new
command clones a repository to use as a template. The -p
option above tells apex
to use a sub-directory and you can use -b
to specify a branch if necessary. The final argument is the directory to create.
The template itself can have configuration defined in a .template
file. Any file with a .tmpl
extension is treated as a text file to render with data defined in the .template
configuration along with user or environment variables.
That’s handy but we have tools that do this. This is only the start.
This example is a basic TypeScript-on-node.js template and includes an apex.axdl
definition and apex.yaml
configuration file. This is what drives apex
after the base project creation.
The yaml configuration starts simply. This one points to the Apexlang definition file (.axdl
files) and specifies the code generator plugin to use.
spec: apex.axdl
plugins:
- 'https://raw.githubusercontent.com/apexlang/codegen/main/src/typescript/plugin.ts'
Plugins dynamically generate further configuration based on the Apexlang definition. Yeah, apex
uses Apexlang to configure apex
configuration.
Plugins are TypeScript files that export a single function. The function takes in the current config along with an Apexlang spec, and spits out a new configuration.
The axdl
file is where the magic is defined. It’s a simple interface definition language that looks like this:
namespace "greeting.v1"
interface Greeter {
sayHello(to: Person): string
}
type Person {
firstName: string
lastName: string
}
It’s inspired by GraphQL and models the parts you need to describe most APIs for languages and services.
With a configuration and an .axdl
file, we can start generating code:
$ apex generate
INFO Writing file ./src/api.ts (mode:644)
INFO Writing file ./src/interfaces.ts (mode:644)
apex generate
creates two newfiles, api.ts
and interfaces.ts
.
interfaces.ts
illustrates how apex
translates Apexlang types to TypeScript code. Apexlang interfaces become TypeScript interfaces and Apexlang types become TypeScript classes.
export interface Greeter {
sayHello(to: Person): string;
}
export class Person {
firstName: string;
lastName: string;
constructor({
firstName = "",
lastName = ""
}: { firstName?: string; lastName?: string } = {}) {
this.firstName = firstName;
this.lastName = lastName;
}
}
In api.ts
, we get boilerplate that we can start to fill out. Our interfaces get base implementations inheriting from and using the types defined in interfaces.ts
. All the imports are handled for you and you are ready to start coding.
import { Greeter, Person } from "./interfaces";
class GreeterImpl implements Greeter {
sayHello(to: Person): string {
return "";
}
}
We can also add documentation to our .axdl
file and it will render as comments in the code.
namespace "greeting.v1"
"A simple greeting service"
interface Greeter {
"Say hello to a Person"
sayHello(to: Person): string
}
"An instance of a person"
type Person {
"The person's first name"
firstName: string
"The person's last name"
lastName: string
}
apex generate
now produces this:
// A simple greeting service
export interface Greeter {
// Say hello to a Person
sayHello(to: Person): string;
}
// An instance of a person
export class Person {
// The person's first name
firstName: string;
// The person's last name
lastName: string;
[rest snipped...]
}
More generators!
Writing what looks like code to generate one instance of other code isn’t that exciting. You could have written the TypeScript by hand. But Apexlang is more than just a code generator. It’s a code generation framework. It’s a templating framework. It’s a documentation framework. It’s a schema framework. It’s a configuration framework. It’s a framework for frameworks.
We can just as easily generate markdown documentation for our API by adding in a custom generates
section. (These are what plugins generate for you but you can add your own manually.)
generates:
API.md:
module: https://deno.land/x/apex_codegen/markdown/mod.ts
config:
title: 'My Awesome Project'
The generates
block is keyed with the file to be generated (i.e. API.md
) and the generator configuration. In this case we're delegating to the markdown
module of the official Apexlang codegen project.
This is the markdown we generate:
# My Awesome Project
Namespace: **`greeting.v1`**
## Interfaces
### **Greeter**
A simple greeting service
- **`sayHello(to: Person) -> string`**: Say hello to a Person
## Types
### **Person**
An instance of a person
- **`firstName: string`** : The person's first name
- **`lastName: string`** : The person's last name
There are generators for C#, Go, TinyGo, Rust, Java, Protobuf, OpenAPI, JSONSchema, Python, and more. If you work in shops with many teams and languages, you must look into Apexlang. It’s a game changer. Specify your interfaces once and generate scaffolding, boilerplate, documentation, bindings, everything from there. You can inherit from the existing Apexlang generators or create entirely new ones. It’s just TypeScript running on Deno and you can host your templates and generators anywhere.
What’s next?
I didn’t start Apexlang but I recognized the value immediately when using it as WIDL. The original author (Phil Kedy) and I have joined up as part of Candle to build tools that make everything about software easier. Once you start it’s hard to stop. We use Apexlang heavily on NanoBus, our WebAssembly-oriented developer platform.
We’re in the process of fully converting Apexlang and its dependencies from a go/node hybrid to deno and WebAssembly. The apex
CLI, parsers, and code generators are now fully deno but there may be some rough edges as we iron out the wrinkles.
We hang out in our discord server and are always happy to chat about Apexlang, WebAssembly, go, rust, or anything else. Come say hi!