Skip to content
On this page

api ​

api is the shared HTTP and domain base every other module sits on. It gives you a JSON envelope, ApiService, ctx.respond, and a global exception handler. npx adonia init installs it together with types, constants, and contracts.

This page covers those four core packages: what they copy, how controllers build responses, and how errors become one envelope.

Add it ​

Run init on an AdonisJS app. You don't add api by itself unless you're recovering a host that skipped core:

bash
npx adonia init --wire
# or, if core is missing later:
npx adonia add api

add api always expands to types, constants, and contracts. init --wire also registers the provider, error handler, import aliases, and an empty config/modules.ts. See host wiring.

api has no stubs, npm dependencies, env keys, or domain events.

What you get ​

The domain slice lands under modules/api/:

FileRole
service.tsApiService base class
envelope.tsRuntime helpers (prepareError, type guards)
exception.tsApiException carrier
exception_handler.tsGlobal JSON exception handler
provider.tsRegisters ctx.respond and ctx.serialize
module.jsonManifest

Core packages copy next to it:

PackageImportContents
types#modules/typesEnvelope types, ModuleActionConfig, IUserModel
constants#constantsexceptions, responseCodes, defineCodes
contracts#modules/contractsShared User and UserStore

Host wiring must provide these import aliases (or init --wire does):

  • #modules/* → ./modules/*.js
  • #modules/types → ./modules/types/index.js
  • #modules/contracts → ./modules/contracts/index.js
  • #constants / #constants/* → ./modules/constants/
  • #adapters/* → ./app/adapters/*.js

Response envelope ​

Controllers and hosts build envelopes. Domain services under modules/ return DTOs and throw ApiException instead of wrapping results.

A success body looks like this:

json
{
  "success": true,
  "message": "M_AUTH_LOGIN",
  "data": { "id": 1, "email": "ada@example.com" }
}

message is a string or null. We recommend the codes in #constants/responseCodes (M_AUTH_LOGIN, M_PROFILE_UPDATED, and the rest) so clients can switch on a stable token.

A paginated success body adds required meta:

json
{
  "success": true,
  "message": null,
  "data": [],
  "meta": {
    "total": 40,
    "page": 1,
    "pageSize": 20,
    "totalPages": 2
  }
}

A failure body looks like this:

json
{
  "success": false,
  "message": "E_INVALID_TOKEN",
  "status": 400
}

Vine validation failures keep the same envelope and add an errors array of field messages. The exception handler takes the first field message as message.

ApiService ​

ApiService is the base class for feature domain services. It exposes envelope helpers for controllers and emitSafe for domain events.

ts
import ApiService from '#modules/api/service'

export class AuthService extends ApiService {
  namespace = 'auth'
}

namespace selects config/modules.ts under modules.<namespace>. If namespace is null (the default on the base class), resolveConfig() returns null and emitSafe logs a warning and returns.

Envelope helpers ​

Call these from controllers and hosts, not from domain services:

  • prepareResponse(data, message?) — success envelope
  • preparePaginatedResponse(data, total, params?) — success plus meta (page defaults to 1, pageSize defaults to data.length)
  • prepareError(message, status?) — failure envelope (also exported from #modules/api/envelope)

emitSafe ​

emitSafe(eventName, payload) emits a typed Adonis event when:

  1. config/modules.ts has an entry for this.namespace.
  2. That entry's emits array includes eventName.

Listener errors are caught and logged. They don't fail the use case. Import the module's events.ts file so the EventsList augmentation loads.

NOTE

add merges each feature module's events list into config/modules.ts. Remove a name from emits when you want to silence that event without changing the service.

ctx.respond ​

The API provider boots HttpContext.respond and HttpContext.serialize. respond takes an envelope, serializes data through Adonis transformers without double-wrapping, and returns the shared JSON shape. On success: false it sets the HTTP status from result.status (default 400).

ts
return ctx.response.ok(
  await ctx.respond({
    success: true,
    message: responseCodes.AUTH_LOGIN.code,
    data: { user, token: sessionResult.token ?? null },
  })
)

Register the provider in adonisrc.ts:

ts
() => import('#modules/api/provider'),

Exception handler ​

#modules/api/exception_handler formats every JSON error through prepareError. Adonis package errors, ApiException, and unknown failures share one shape. debug is forced off so clients get a stable envelope in every environment.

Status codes 400, 401, 403, 404, and 422 are ignored in report so expected client errors stay quiet.

Point the HTTP server at it in start/kernel.ts:

ts
server.errorHandler(() => import('#modules/api/exception_handler'))

init --wire also writes app/exceptions/handler.ts as a re-export of that module.

Throw domain errors with ApiException:

ts
import ApiException from '#modules/api/exception'
import { exceptions } from '#constants/exceptions'

throw ApiException.from(exceptions.INVALID_TOKEN)

Exception catalog ​

#constants/exceptions is the shared code table. Feature services throw some of these; others are documented for host middleware (auth guards, rate limiters, upload limits). Hosts extend the table with defineCodes in app/constants/.

KeyHTTPCodeThrown by
INVALID_CREDENTIALS401E_INVALID_CREDENTIALSHost auth
UNAUTHENTICATED401E_UNAUTHENTICATEDHost auth
SESSION_EXPIRED401E_SESSION_EXPIREDHost auth
ACCOUNT_UNVERIFIED403E_ACCOUNT_UNVERIFIEDauth
FORBIDDEN403E_FORBIDDENHost authorization
SAME_EMAIL400E_SAME_EMAILaccount
INVALID_PASSWORD400E_INVALID_PASSWORDaccount
INVALID_TOKEN400E_INVALID_TOKENauth, account
PAYLOAD_TOO_LARGE413E_PAYLOAD_TOO_LARGEHost uploads
EMAIL_EXISTS409E_EMAIL_EXISTSaccount
TOO_MANY_ATTEMPTS429E_TOO_MANY_ATTEMPTSHost limiter
TOO_MANY_REQUESTS429E_TOO_MANY_REQUESTSHost limiter

The exception handler puts error.code (or error.message) in message. Clients therefore see E_INVALID_TOKEN, not a localized sentence, unless you throw with an explicit message option.

Shared UserStore ​

#modules/contracts exports User, RegisterUserInput, AuthMethod, and UserStore. auth and account depend on this port. The Lucid adapter is an auth stub, not part of api.

UserStore must:

  • Persist new email users without setting emailVerifiedAt or the verification token (the auth service owns that flow).
  • Hash passwords on create and updatePassword.
  • Map persistence models to the portable User DTO on the way out.

Next steps ​

Wire a host, then add a feature module:

  1. Confirm host wiring registered the provider and exception handler.
  2. Add auth for accounts, or notification if you only need dispatch.
  3. Read module.json when you author your own slice.