Interfaces
What Is an Interface
An interface is a contract that defines how modules communicate. The contract declares functions, events, or registrations without implementing them. Producers fulfill the contract, and consumers call the contract — neither side knows about the other directly.
The AntelopeJS core resolves which module fulfills each interface at runtime. A module that needs email delivery imports the email interface, not the email module. Modules depend on contracts rather than on each other, which keeps them independent and interchangeable.
What an Interface Contains
An interface file is more than type definitions and proxy declarations. The file serves as the complete public surface that both producers and consumers rely on.
An interface can contain:
- Type definitions — TypeScript types and interfaces shared between producer and consumer
- Proxy instances —
InterfaceFunction,EventProxy, andRegisteringProxydeclarations that define the contract points - Wrapper functions — Public API functions that call the underlying proxies, providing a cleaner API surface
- Decorator factories — Decorators used by consumers (e.g.,
@Controller,@Authentication) - Utility code — Helper classes, query builders, metadata management — any generic code that stays the same regardless of the implementation
The following example shows an interface file with shared types, internal proxies, and public wrapper functions:
import { InterfaceFunction } from "@antelopejs/interface-core";
// Types shared between producer and consumer
export interface EmailParams {
to: string;
subject: string;
body: string;
}
export interface EmailResult {
messageId: string;
status: string;
}
// Internal proxies — the actual contract points
export namespace internal {
export const sendEmailProxy = InterfaceFunction<(params: EmailParams) => Promise<EmailResult>>();
export const getStatusProxy = InterfaceFunction<(messageId: string) => Promise<string>>();
}
// Public API — clean wrapper functions for consumers
export function SendEmail(params: EmailParams): Promise<EmailResult> {
return internal.sendEmailProxy(params);
}
export function GetEmailStatus(messageId: string): Promise<string> {
return internal.getStatusProxy(messageId);
}
Consumers call SendEmail(params) instead of reaching for the proxy directly. The wrapper functions provide a stable, readable API while the proxies handle the runtime resolution behind the scenes.
Embedded vs Standalone Interfaces
Interfaces take one of two forms depending on how many modules need to implement them.
Embedded Interface
An embedded interface lives inside the module that implements the contract, in a src/interface/ directory. The module contains both the contract and its implementation. Embedded interfaces are the natural starting point when you create a new module with a new interface.
my-module/
├── src/
│ ├── index.ts
│ ├── interface/
│ │ └── index.ts # Interface contract
│ └── implementations/
│ └── index.ts # Implementation
The module entry point connects the contract to its implementation:
import { ImplementInterface } from "@antelopejs/interface-core";
export async function construct() {
ImplementInterface(
await import("./interface"),
await import("./implementations")
);
}
Standalone Interface Package
When an interface needs multiple implementations, extract the contract into its own npm package. The standalone interface package contains only the contract — types, proxies, wrappers, and utilities. Each implementing module lives in a separate package and declares the interface in its implements field.
@antelopejs/interface-email/ # Standalone interface package
├── src/
│ └── index.ts # Types, proxies, wrappers
@antelopejs/email-brevo/ # Implementation module A
├── src/
│ ├── index.ts
│ └── implementations/
│ └── email/
│ └── index.ts
@antelopejs/email-sendgrid/ # Implementation module B
├── src/
│ ├── index.ts
│ └── implementations/
│ └── email/
│ └── index.ts
Each implementing module references the external interface package in its configuration:
{
"antelopeJs": {
"implements": ["@antelopejs/interface-email"]
}
}
The module entry point imports the interface from the standalone package instead of a local path:
import { ImplementInterface } from "@antelopejs/interface-core";
export async function construct() {
ImplementInterface(
await import("@antelopejs/interface-email"),
await import("./implementations/email")
);
}
When to Extract
Start with an embedded interface and extract to a standalone package when the situation calls for it. Three signals indicate that extraction makes sense:
- A second module needs to implement the same interface
- Consumers should depend on the contract without depending on a specific implementation
- The interface becomes part of the public ecosystem and needs its own release cycle
Complete Example
The following example walks through a storage interface from declaration to consumption. It covers three files: the interface contract, the producer module that implements it, and a consumer module that uses it.
Step 1 — Declare the interface
The interface defines a key-value storage contract with get and set operations. InterfaceFunction creates typed proxies, and wrapper functions provide the public API.
import { InterfaceFunction } from "@antelopejs/interface-core";
// Shared types
export interface StorageEntry {
key: string;
value: string;
updatedAt: number;
}
// Internal proxies — the contract points
export namespace internal {
export const getProxy = InterfaceFunction<(key: string) => Promise<StorageEntry | null>>();
export const setProxy = InterfaceFunction<(key: string, value: string) => Promise<StorageEntry>>();
}
// Public API — consumers call these functions
export function Get(key: string): Promise<StorageEntry | null> {
return internal.getProxy(key);
}
export function Set(key: string, value: string): Promise<StorageEntry> {
return internal.setProxy(key, value);
}
Step 2 — Implement in the producer module
The producer module exports a namespace that matches the interface's internal namespace. The core binds each proxy to the corresponding function by matching names.
import type { StorageEntry } from "../interface";
const store = new Map<string, StorageEntry>();
export namespace internal {
export async function getProxy(key: string): Promise<StorageEntry | null> {
return store.get(key) ?? null;
}
export async function setProxy(key: string, value: string): Promise<StorageEntry> {
const entry: StorageEntry = { key, value, updatedAt: Date.now() };
store.set(key, entry);
return entry;
}
}
The module entry point connects the interface to its implementation during the construct phase.
import { ImplementInterface } from "@antelopejs/interface-core";
export async function construct() {
ImplementInterface(
await import("./interface"),
await import("./implementations")
);
}
Step 3 — Consume from another module
A consumer module imports the wrapper functions from the interface and calls them directly. The consumer has no dependency on the producer module — only on the contract.
import { Get, Set } from "@your-org/storage-module/interface";
export async function start() {
await Set("greeting", "hello world");
const entry = await Get("greeting");
if (entry) {
console.log(entry.value); // "hello world"
}
}
If the proxy has no implementation attached yet when the consumer calls Get or Set, the call queues automatically and resolves once a producer module attaches its implementation. This queuing behavior means module load order does not matter.
See also
- Proxies — How proxy types handle inter-module communication
- Exporting Interfaces — How to export interfaces from your module
Architecture
Understand the core architecture of Antelopejs — interfaces, modules, and the core — and how they create a flexible, modular system.
Modules
Modules are the building blocks of an Antelopejs application — self-contained packages with a defined lifecycle, configuration, and interface bindings.