Skip to content

lcecchetti/apiwire

Repository files navigation

apiwire

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.

Why apiwire

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 wiring
  • wire(...) 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.

Quick Start

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);

Error Flow

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"
}

Incremental Adoption

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.

Install

npm install apiwire

Compiler 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:

  • experimentalDecorators Enables the legacy TypeScript decorators that apiwire uses today.
  • emitDecoratorMetadata Emits the constructor type metadata that apiwire reads 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.

Package Surfaces

  • apiwire Main entrypoint for decorators, DI, HttpError, and the Express convenience exports.
  • apiwire/core Adapter-neutral decorators, metadata, and DI only.
  • apiwire/express Express integration through wire(...).

In This Repo

This repo is a monorepo with:

Useful commands:

npm install
npm run build
npm run test
npm run dev:demo-express

The demo server runs on http://localhost:3000.

Read More

About

A simple and pluggable way to organize Node.js REST APIs with decorators and dependency injection.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors