Skip to content

Layout: Module vs Feature

Open in ChatGPTOpen in Claude

Axis supports two first-class directory layouts, chosen at scaffold time with --layout and recorded in axis.json. The framework doesn’t care which you pick — its only contract is the registries (Project Structure) — but tooling like axs add reads the recorded layout to place generated code correctly.

Lambdas are grouped by module, then by kind, under lambdas/modules/; tables live centrally in config/tables/:

lambdas/modules/users/api/create/{index,service}.ts
lambdas/modules/jobs/workers/process/{index,service}.ts
config/tables/billing.ts

This is the validated, battle-tested convention, it is the default, and it is valid for every starter. It suits small-to-medium REST apps where “show me all the API handlers” is the common navigation question.

Lambdas are colocated by feature under lambdas/features/, with no kind segment, and a feature’s tables sit alongside its code (re-exported from the central config/tables/index.ts registry so the framework contract is unchanged):

lambdas/features/users/create/{index,service}.ts
lambdas/features/jobs/process/{index,service}.ts
lambdas/features/billing/tables/billing.ts ← colocated, registry re-exports it

This suits domain-driven teams where “show me everything about billing” is the common question.

Commandmodule layoutfeature layout
axs add lambda users/create --kind api --method POST --path /userslambdas/modules/users/api/create/lambdas/features/users/create/
axs add lambda jobs/process --kind workerlambdas/modules/jobs/workers/process/lambdas/features/jobs/process/
axs add table billingconfig/tables/billing.ts + registry entrylambdas/features/billing/tables/billing.ts + re-export

The placement rules come straight from axis.json (paths.lambdas, conventions.lambdaGrouping, conventions.tableLocation) — never from inspecting the directory tree, which would be fragile on a fresh or customised project.

In this version, feature layout is supported for the rest-v1, rest-v2, graphql, and worker starters. Only bare is module-only:

Startermodulefeature
rest-v1✅ (default)
rest-v2✅ (default)
graphql✅ (default)
worker✅ (default)
bare✅ (default)❌ not supported

Passing an unsupported pairing fails fast at the CLI with an explicit error rather than scaffolding something half-supported:

[Axis] --layout feature is not supported with --template bare in this version.
Starters supporting --layout feature: rest-v1, rest-v2, graphql, worker.

Why the scope: every supported (starter, layout) pair is backed by a full CI-validated example project, so the matrix only lists what is genuinely validated. rest-v1, rest-v2, graphql, and worker each ship a feature twin — multi-module REST breadth for the first, the lean /{proxy+} v2 surface for the second, domain-grouped resolvers with colocated artifacts for the third, and job-domain grouping for the last. The rest-v2 twin exists mainly for matrix completeness: its single greedy /{proxy+} topology means the layout choice has less leverage than for rest-v1. bare is a single-Lambda floor with nothing to group, so it stays module-only by design — permanently, not pending. Because layout is an extensible enum in the manifest, adding pairings later breaks nothing.

Default to module. Choose feature for a rest-v1, rest-v2, graphql, or worker project when your team organises ownership by domain and wants each feature’s handlers and tables in one place. On a graphql project the feature layout additionally colocates resolver artifacts (.vtl / APPSYNC_JS .js) at lambdas/features/<feature>/resolvers/, recorded as conventions.resolverLocation: "colocated"; the SDL stays central at assets/schema.graphql because one schema is the cross-feature contract. Both layouts are fully supported by axs add, the SDK generator, and the rest of the tooling — this is a navigation preference, not a capability trade.

© 2026 Axis Tech. Powered by axis-tech.co.