← Back to list

9 Rules Every API Architecture Standard Should Define

The not so small decisions that keep your API from drifting.

Razvan Ludosanu in Dev Genius · 2026-07-03 13:55 · 25 claps · 5.4 min read paywalled
#api #software-architecture #nodejs #expressjs #backend-development
Open on Medium ↗
Wiki topics: CRY · Crypto & Web3 🌐 · Web Development 📰 · Journalism & News 🏛️ · Architecture

9 Rules Every API Architecture Standard Should Define

The not so small decisions that keep your API from drifting.

This article expands on the standard behind **The Express Brew Kit**, a reusable asset for structuring Express APIs without improvising.

Not a member? Read this for **free**.

If you have built an API before, you have probably had to answer questions like these:

  • Where should this new file go?
  • Where should this logic live?
  • How should this endpoint validate input?
  • How should this endpoint access the database?
  • Should this response follow the same shape as the others?

Without an architecture standard, these questions usually get answered while coding. Each endpoint provides its own answer and the API slowly drifts into an amalgam of anti-patterns.

Here are the 9 most important rules every API standard should define before the first line of code is ever written, so the codebase doesn’t have to rediscover its own structure every time a feature is added.

1. File Structure

A file structure rule defines where each type of module belongs in the application.

It allows developers to know where each part of a new endpoint belongs before the implementation starts. It also makes the project faster to scan and to review, as misplaced responsibilities become visible faster.

For example, a common implementation is to organize files by technical responsibility, which keeps the architecture visually explicit — by looking at the directory tree, you immediately know where each layer lives.

./
└─ src/
   ├─ controllers
   │  └─ user-login.js
   ├─ repositories
   │  └─ user.js
   ├─ routers
   │  └─ authentication.js
   ├─ schemas
   │  └─ user-login.js
   ├─ services
   │  └─ user-login.js

2. Layer Boundaries

A layer boundary rule defines what each layer of the application is responsible for.

It makes the system easier to extend because each layer has a smaller and more stable responsibility. It also makes the code easier to test, because business behavior is not tied directly to the framework’s request and response objects.

A common implementation is to separate the application into 3 layers:

  • The router layer that contains the API routes of the application and is responsible for translating the result of the call made to the service layer into a valid HTTP response.
  • The service layer that contains the business logic of the application and is responsible for performing application-specific tasks, and calling the data access layer or third-party services.
  • The data access layer that contains the persistence modules of the application and is responsible for executing the persistence operations such as reads and writes on the database.
export default function createUserLoginController({ userLoginService }) {
  return async function userLoginController(req, res) {
    const token = await userLoginService(req.payload);

    res.status(200).json({
      data: { token }
    });
  };
}

3. Naming Conventions

A naming convention rule defines how files, functions, modules, and architecture pieces should be named.

It allows related files and modules to be easily found and connected mentally across the various layers of the application, which in turn allows developers to spend less time translating intent and more time understanding the implementation.

For example, a common convention is to use the same action-oriented name across the files involved in a feature.

schemas/user-login.js
controllers/user-login.js
services/user-login.js

4. Configuration Policy

A configuration policy defines how environment values are loaded, validated, mapped, and exposed to the application.

It allows the application to validate its runtime requirements before it starts, so that invalid ports, missing database credentials, or malformed external service settings can be caught early instead of failing later during request execution.

It also makes startup behavior more predictable and gives the rest of the codebase one clean configuration object to depend on instead of reading raw environment variables everywhere.

For example, a common implementation is to validate environment variables with a schema:

Joi.object({
  NODE_ENV: Joi.string()
    .valid('development', 'production', 'test')
    .required(),
  SERVER_PORT: Joi.number()
    .port()
    .default(3000),
  DATABASE_HOST: Joi.string()
    .required(),
  DATABASE_PORT: Joi.number()
    .port()
    .required()
});

And then map them into a clean and organized configuration object used by the rest of the application:

{
  environment: value.NODE_ENV,
  server: {
    port: value.SERVER_PORT
  },
  database: {
    host: value.DATABASE_HOST,
    port: value.DATABASE_PORT
  }
}

5. Request Lifecycle

A request lifecycle rule defines how an HTTP request moves through the application from entry point to response.

It allows every endpoint to follow a predictable execution path, so that developers know where the request is parsed, where validation happens, where business behavior runs, where persistence is accessed, and where the response is created.

It also makes debugging easier because the request moves through known steps instead of jumping through unrelated parts of the codebase.

For example, a common implementation in Express is to move the request through the following components, and return the final HTTP response from the controller.

Client
→ API
→ Application-level middleware chain (e.g., CORS, request tracing)
→ Router
→ Route-level middleware chain (e.g., parsing, validation)
→ Request handler
→ Service
→ Repository / Provider

6. Request Validation

A request validation rule defines where input is checked, what parts of the request are validated, and how invalid data is rejected.

It allows invalid input to be rejected before it reaches the core application, so that services can work with data that has already passed the API boundary checks.

It also makes validation errors more consistent as malformed params, query values, and request bodies are handled through the same path.

For example, a common implementation is to define a schema for the expected request payload:

export default {
  body: Joi.object({
    email: Joi.string().email().required(),
    password: Joi.string().min(8).required()
  })
};

And mount a validation middleware that uses this schema before the controller:

router.post(
  '/login',
  express.json(),
  requestValidatorMiddleware('userLogin'),
  userLoginController
);

7. Data Persistence

A data persistence rule defines how the application reads from and writes to the database.

It allows business logic to ask for the data operation it needs without depending on ORM details, which makes the database layer easier to change because its details are contained behind a clear boundary.

For example, a common implementation is to expose database operations through repositories:

export default function createUserRepository({ userModel }) {
  return {
    findByEmail(email) {
      return userModel.findOne({
        where: { email }
      });
    }
  };
}

Which allows the service to ask for a user by email without knowing how the query is built:

const user = await userRepository.findByEmail(email);

8. Response Contract

A response contract defines how successful API responses are shaped.

It allows clients to receive successful responses through a predictable structure and keeps response formatting at the HTTP boundary, so services can return application results without knowing how JSON should be shaped for the client.

For example, a common implementation is to wrap successful response data inside a predictable data object and identify the stored values under explicit keys.

res.status(200).json({
  data: {
    token
  }
});

9. Error Contract

An error contract defines how application failures are normalized and returned to clients.

It allows the API to treat failures as a consistent part of its contract, so that clients can handle failed requests through a predictable structure, the same way they handle successful responses.

It also makes debugging easier because every error exposes the same kind of information.

For example, a common implementation is to throw known HTTP errors:

throw new HTTPConflictError({
  details: 'This email address is already registered'
});

And let centralized error middleware turn them into a standard response:

{
  "error": {
    "code": "CONFLICT",
    "message": "Conflict",
    "details": "This email address is already registered",
    "requestId": "b8aa75a2-dc9c-4c5b-a22c-aeabb9b70cec"
  }
}

Final Thoughts

A good API architecture standard doesn’t need to be complicated, but it needs to make the important decisions explicit to prevent the API from drifting.

When these rules are missing, every new endpoint has room to introduce a slightly different structure, response shape, validation style, or persistence pattern.

This is the core idea behind the **Express Brew Kit**.

[embed]The Express.js Brew Kit | Backend Brewery A reusable standard for structuring, extending, and shipping Express APIs without reinventing the architecture from…backendbrewery.dev

It gives you a reusable Express API standard where these decisions are already made, explained, and wired into a working blueprint, so every new endpoint has a clear structure to follow before you or AI start writing code.

If you enjoyed this story, a clap or share goes a long way.

Thanks for reading.


메타데이터
post_id
b4e6e0b5f720
slug
9-rules-every-api-architecture-standard-should-define-b4e6e0b5f720
url
https://blog.devgenius.io/9-rules-every-api-architecture-standard-should-define-b4e6e0b5f720
canonical_url
https://blog.devgenius.io/9-rules-every-api-architecture-standard-should-define-b4e6e0b5f720
author_url
https://medium.com/@backendbrewery
status
ok
fetched_at
2026-07-08 21:20:17