Your API, minus the handler jungle.
apiwire is a simple and pluggable way to organize Node.js APIs with decorators and dependency injection. Controllers describe routes, services hold reusable logic, and the HTTP adapter stays at the edge where it belongs.
If your Express app is starting to feel like a pile of handlers, apiwire gives you a cleaner shape without dragging you into framework theater.
Most REST APIs start out fine, then drift into this:
- route files doing transport work and business work at the same time
- services manually stitched together across the app
- request parsing and response shaping repeated everywhere
- no clean boundary between your application code and your HTTP layer
apiwire keeps that split explicit:
@Controller()groups related endpoints@Get(),@Post(), and friends define routes where they belong- parameter decorators like
@Body(),@Param(), and@Query()map request state into method arguments @Injectable()makes services container-managed, so dependencies flow through constructors instead of manual wiringwire(...)mounts everything into Express with one setup call
That is the whole idea: classes for structure, decorators for API shape, DI for composition, adapters for framework integration.
import express from 'express';
import { Controller, Get, Injectable } from 'apiwire';
import { wire } from 'apiwire/express';
@Injectable()
class GreetingService {
getMessage(name = 'world') {
return `Hello ${name} from apiwire`;
}
}
@Controller('/hello')
class HelloController {
constructor(private greetingService: GreetingService) {}
@Get()
index() {
return {
message: this.greetingService.getMessage(),
};
}
}
const app = express();
app.use(express.json());
wire(app, {
controllers: [HelloController],
});
app.listen(3000);The default pattern is intentionally simple:
- controller return values become the response body
- thrown errors become the error response
- generic errors become
500
For explicit HTTP errors:
import { HttpError } from 'apiwire';
throw new HttpError('User not found', 404, {
code: 'xx',
});By default, that becomes:
{
"message": "User not found",
"code": "xx"
}apiwire can be adopted one route group at a time inside a normal Express app.
import express from 'express';
import { wire } from 'apiwire/express';
const app = express();
app.get('/legacy/health', (_req, res) => {
res.json({ ok: true });
});
wire(app, {
controllers: [UsersController],
prefix: '/api',
});Legacy Express routes and apiwire controllers can live together just fine.
npm install apiwireCompiler options:
Add these to your app tsconfig.json, or to the shared tsconfig that your app extends:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}Why they matter:
experimentalDecoratorsEnables the legacy TypeScript decorators thatapiwireuses today.emitDecoratorMetadataEmits the constructor type metadata thatapiwirereads for automatic dependency injection.
apiwire uses the legacy decorator model because the newer standard decorators do not provide the emitted constructor metadata it needs for automatic dependency injection.
apiwireMain entrypoint for decorators, DI,HttpError, and the Express convenience exports.apiwire/coreAdapter-neutral decorators, metadata, and DI only.apiwire/expressExpress integration throughwire(...).
This repo is a monorepo with:
packages/apiwireThe publishable package.apps/demo-expressA small Express app that usesapiwirelocally.
Useful commands:
npm install
npm run build
npm run test
npm run dev:demo-expressThe demo server runs on http://localhost:3000.
- Package docs: packages/apiwire/README.md
- Demo notes: apps/demo-express/README.md