no-external-api-calls-in-utils
Report network calls inside utility modules, where an HTTP client makes the helper untestable without the network.
Report a network call made inside a utility module. A helper that reaches the network cannot be tested, reused or run offline without it; the client belongs in a service, or is passed in.
This rule is part of eslint-plugin-modularity.
Quick Summary
| Aspect | Details |
|---|---|
| Severity | Error (code quality) |
| Auto-Fix | ❌ No |
| Category | Modularity |
| Suggestions | ✅ Inject the client, extract to a service, pass the client |
Rule Details
In a utility file, the rule reports a call whose callee resolves, by its binding, to an HTTP client:
- the global
fetch, also writtenglobalThis.fetch/window.fetch; - a value imported or required from one of
httpModules(axios, got, ky, node-fetch, undici, superagent, needle, phin,http,https,http2, with or without thenode:prefix), called directly (axios(config)) or through one ofnetworkMethods(axios.get(url),axios['post'](url),https.get(url)); - a client derived from one by a
const(const api = axios.create(), thenapi.get(url)).
It does not report:
- a client passed in as a parameter. Injecting the client is the fix this rule
suggests, so
function load(url, fetch) { return fetch(url); }andfunction load(url, axios) { return axios.get(url); }are not findings, even when an import of the same name exists in an outer scope; - other methods of a client (
axios.create(),axios.isAxiosError(e)), aMap#get, or a function imported from a module that is not an HTTP client (lodash'sget); - a test file (
*.test.*,*.spec.*) whileignoreInTestsis true; - any file outside
utilityPatterns.
An undeclared identifier spelled like a client module (axios.get(url) with no
import in the file) is still reported, for the CDN global it most often is.
Configuration
| Option | Type | Default | Description |
|---|---|---|---|
utilityPatterns | string[] | ['**/utils/**', '**/helpers/**', '**/lib/**'] | Globs (** any number of directories, * within one segment) for the files the rule checks |
httpModules | string[] | the list above | Modules whose imports and requires are HTTP clients |
networkMethods | string[] | get post put patch delete head options request fetch all stream | Methods that send a request on a client. A dotted entry ('httpClient.get') names an in-house client exactly, by name |
ignoreInTests | boolean | true | Skip *.test.* / *.spec.* files |
Examples
❌ Incorrect
// src/utils/user.ts
import axios from 'axios';
export async function loadUser(id: string) {
const { data } = await axios.get(`/api/users/${id}`);
return data;
}
export const ping = () => fetch('/health');✅ Correct
// src/utils/user.ts: the client is passed in
export async function loadUser(id: string, http: { get(url: string): Promise<{ data: unknown }> }) {
const { data } = await http.get(`/api/users/${id}`);
return data;
}
// src/services/user-service.ts: services own the network
import axios from 'axios';
export const loadUser = (id: string) => axios.get(`/api/users/${id}`);Configuration Examples
// eslint.config.mjs
export default [
{
rules: {
'modularity/no-external-api-calls-in-utils': [
'error',
{ utilityPatterns: ['**/shared/**'], networkMethods: ['get', 'post', 'apiClient.get'] },
],
},
},
];Related Rules
no-mutable-exports- Keep module interfaces stable
Known False Negatives
- A wrapper the rule cannot trace (
import { http } from './http', an in-house client) is not recognised; name it innetworkMethodsas'http.get'. - A client destructured from an import (
const { get } = axios) is not followed.
Use this rule
npm install -D oxlint-plugin-modularity{ "jsPlugins": ["oxlint-plugin-modularity"], "rules": { "modularity/no-external-api-calls-in-utils": "error" }}import { defineConfig } from 'vite-plus';export default defineConfig({ lint: { jsPlugins: ['oxlint-plugin-modularity'], rules: { 'modularity/no-external-api-calls-in-utils': 'error', }, },});oxlint-plugin-modularity re-exports eslint-plugin-modularity/oxlint: the same rules under the same IDs, and no ESLint install. Why oxlint.
Did this rule catch something? Star the repo to get new CWE coverage as we ship it — or follow the AI-code-security benchmarks behind these rules.