Skip to content

Commit b174ad3

Browse files
author
Andrea Cosentino
committed
feat: add option to exclude pure api fs
1 parent c3ca8b0 commit b174ad3

6 files changed

Lines changed: 100 additions & 4 deletions

File tree

docs/api/index.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,34 @@ universalApi({
8282
If the directory doesn't exist, file-based routing will be disabled automatically, but the plugin will still work for custom handlers.
8383
:::
8484

85+
### disablePureFsApi
86+
87+
- **Type**: `boolean`
88+
- **Default**: `false`
89+
90+
Disable the **pure File-System API** — the automatic fallback that maps incoming requests directly to files inside `fsDir` when no REST handler matches.
91+
92+
When set to `true`, only REST handlers explicitly configured with `handle: "FS"` continue to read files from `fsDir`. Every other unmatched request is handled according to [`noHandledRestFsRequestsAction`](#nohandledrestfsrequestsaction) (default: `"404"`).
93+
94+
> **This option does not affect handlers whose `handle` property is `"FS"`**. Those always keep their file-system behaviour, regardless of this flag.
95+
96+
```typescript
97+
universalApi({
98+
fsDir: 'mock',
99+
disablePureFsApi: true, // only explicit FS handlers can reach the filesystem
100+
handlers: [
101+
{ pattern: '/api/users', method: 'GET', handle: 'FS' }
102+
]
103+
})
104+
105+
// GET /api/users → served from mock/api/users.json ✓ (explicit FS handler)
106+
// GET /api/orders → 404 / forwarded ✓ (pure FS fallback disabled)
107+
```
108+
109+
::: tip When to use this option
110+
Use `disablePureFsApi: true` when you want explicit control over which endpoints are exposed and want to prevent the plugin from automatically serving any file that happens to exist under `fsDir`.
111+
:::
112+
85113
### enablePreview
86114

87115
- **Type**: `boolean`
@@ -452,6 +480,7 @@ export default defineConfig({
452480
logLevel: 'debug',
453481
endpointPrefix: '/api',
454482
fsDir: 'mock',
483+
disablePureFsApi: false, // set to true to expose only explicit FS handlers
455484

456485
// Preview behaviour
457486
enablePreview: false, // Disable mocks when running vite preview

docs/guide/file-system-api.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,35 @@ const users = Array.from({ length: 100 }, (_, i) => ({
352352
// Save to mock/users.json
353353
```
354354

355+
## Disabling the Pure File-System API
356+
357+
By default, any request that reaches the plugin and does not match a REST handler is automatically resolved against `fsDir` (the pure File-System API). You can turn off this automatic fallback with the `disablePureFsApi` option.
358+
359+
```typescript
360+
universalApi({
361+
fsDir: 'mock',
362+
disablePureFsApi: true, // disable the automatic FS fallback
363+
handlers: [
364+
{ pattern: '/api/users', method: 'GET', handle: 'FS' }
365+
]
366+
})
367+
```
368+
369+
When `disablePureFsApi` is `true`:
370+
371+
- Requests that match a handler with `handle: "FS"` are **still served from `fsDir`** as usual.
372+
- Requests that do **not** match any handler are handled according to `noHandledRestFsRequestsAction` (`"404"` by default) — they are **never** automatically mapped to files.
373+
374+
| Request | `disablePureFsApi: false` (default) | `disablePureFsApi: true` |
375+
|---------|--------------------------------------|--------------------------|
376+
| Matches a handler with `handle: "FS"` | Served from `fsDir`| Served from `fsDir`|
377+
| Matches a custom handler | Custom logic ✓ | Custom logic ✓ |
378+
| No handler matched | Automatic FS lookup ✓ | 404 / forward ✓ |
379+
380+
::: tip When to use this option
381+
Use `disablePureFsApi: true` when you want strict, explicit control over which endpoints are exposed and you want to prevent the plugin from automatically serving any file that happens to exist under `fsDir`.
382+
:::
383+
355384
## Limitations
356385

357386
- ❌ POST with multiple files not supported (only first file is written)

src/models/plugin.model.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1588,6 +1588,39 @@ type UniversalApiBaseOptions = {
15881588
*/
15891589
fsDir?: string | null;
15901590

1591+
/**
1592+
* Disable the **pure File-System API** — the automatic fallback that serves
1593+
* files directly from `fsDir` when no REST handler matches an incoming
1594+
* request.
1595+
*
1596+
* When `true`, only REST handlers explicitly configured with `handle: "FS"`
1597+
* continue to read files from `fsDir`; every other unmatched request is
1598+
* handled according to `noHandledRestFsRequestsAction` (default: `"404"`).
1599+
*
1600+
* This option has **no effect** on handlers whose `handle` property is set
1601+
* to `"FS"` — those always keep their file-system behaviour regardless of
1602+
* this flag.
1603+
*
1604+
* Setting `disablePureFsApi: true` is useful when you want precise, explicit
1605+
* control over which endpoints are exposed without relying on the implicit
1606+
* directory-to-route mapping.
1607+
*
1608+
* @default false
1609+
*
1610+
* @example
1611+
* // Serve files only through explicitly declared FS handlers
1612+
* {
1613+
* fsDir: './mock-data',
1614+
* disablePureFsApi: true,
1615+
* handlers: [
1616+
* { pattern: '/api/users', method: 'GET', handle: 'FS' }
1617+
* ]
1618+
* }
1619+
* // GET /api/users → served from mock-data/api/users.json ✓ (explicit FS handler)
1620+
* // GET /api/orders → 404 or forwarded ✓ (pure FS disabled)
1621+
*/
1622+
disablePureFsApi?: boolean;
1623+
15911624
/**
15921625
* Enable the plugin in preview mode (`vite preview`).
15931626
* When `false`, the plugin does not intercept requests in preview mode.
@@ -1884,6 +1917,7 @@ export type UniversalApiOptions = UniversalApiBaseOptions & (
18841917
/** @internal */
18851918
export type UniversalApiOptionsRequired = Omit<Required<UniversalApiOptions>, "handlerMiddlewares" | "endpointPrefix"> & {
18861919
endpointPrefix: string[];
1920+
disablePureFsApi: boolean;
18871921
/**
18881922
* Prefixes that were provided by the user but rejected because they resolve
18891923
* to the root path ("/") after normalisation. Stored so that runAsyncInit

src/plugin/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export function universalApiPlugin(opts?: UniversalApiOptions): Plugin {
2626
wsHandlers: [],
2727
pagination: null,
2828
filters: null,
29+
disablePureFsApi: false,
2930
config: {} as UniversalApiOptionsRequired["config"],
3031
matcher: {} as UniversalApiOptionsRequired["matcher"],
3132
};

src/utils/plugin.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -598,7 +598,7 @@ async function handlingApiRestRequest(logger: ILogger, matcher: AntPathMatcher,
598598
}
599599

600600
const runPluginInternal = async (req: IncomingMessage, res: ServerResponse, logger: ILogger, options: UniversalApiOptionsRequired) => {
601-
const { config, endpointPrefix, handlers, matcher, middlewares, errorMiddlewares, delay, fullFsDir, filters, pagination, parser } = options;
601+
const { config, endpointPrefix, handlers, matcher, middlewares, errorMiddlewares, delay, fullFsDir, filters, pagination, parser, disablePureFsApi } = options;
602602
const fullUrl = Utils.request.buildFullUrl(req, config);
603603
const endpointNoPrefix = Utils.request.removeEndpointPrefix(fullUrl.pathname, endpointPrefix);
604604
let requ: UniversalApiRequest<any> = req as UniversalApiRequest<any>;
@@ -625,9 +625,11 @@ const runPluginInternal = async (req: IncomingMessage, res: ServerResponse, logg
625625
return result;
626626
}
627627

628-
handled = await handlingApiFsRequest(logger, fullUrl, request, res, pagination, filters, parser, null, endpointPrefix, fullFsDir, result);
629-
if (handled) {
630-
return result;
628+
if (!disablePureFsApi) {
629+
handled = await handlingApiFsRequest(logger, fullUrl, request, res, pagination, filters, parser, null, endpointPrefix, fullFsDir, result);
630+
if (handled) {
631+
return result;
632+
}
631633
}
632634

633635
throw new UniversalApiError(`Impossible handling request with url ${fullUrl}`, "NO_HANDLER", fullUrl.pathname);

src/utils/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export const Utils = {
117117
wsHandlers: opts?.wsHandlers ?? [],
118118
pagination: opts?.pagination ?? null,
119119
filters: opts?.filters ?? null,
120+
disablePureFsApi: opts?.disablePureFsApi ?? false,
120121
config,
121122
matcher: new AntPathMatcher()
122123
};

0 commit comments

Comments
 (0)