File Uploads
Nile handles multipart form-data uploads through the same single POST endpoint. Files are parsed, validated through a 7-step chain, and delivered to your action handler as a structured payload.
Configuration
Enable uploads in your REST config:
const server = createNileServer({
name: "MyApp",
services: [/* ... */],
rest: {
baseUrl: "/api/v1",
allowedOrigins: ["http://localhost:3000"],
uploads: {
enforceContentType: true,
limits: {
maxFiles: 5,
maxFileSize: 5 * 1024 * 1024, // 5MB per file
minFileSize: 1, // reject zero-byte files
maxTotalSize: 20 * 1024 * 1024, // 20MB total
maxFilenameLength: 128,
},
allow: {
mimeTypes: ["image/png", "image/jpeg", "application/pdf"],
extensions: [".png", ".jpg", ".jpeg", ".pdf"],
},
},
},
});
Upload Config Options
Sending Uploads
Form-data requests must include the RPC routing fields (intent, service, action) as string fields alongside file fields.
Using the Nile Client
import { createNileClient } from "@nilejs/client";
const nile = createNileClient({ baseUrl: "http://localhost:8000/api/v1" });
const { error, data } = await nile.upload({
service: "documents",
action: "upload",
files: {
document: new File(["content"], "report.pdf", { type: "application/pdf" }),
},
fields: {
title: "Q4 Report",
category: "finance",
},
});
Using fetch Directly
const formData = new FormData();
formData.append("intent", "execute");
formData.append("service", "documents");
formData.append("action", "upload");
formData.append("title", "Q4 Report");
formData.append("document", file);
const response = await fetch("http://localhost:8000/api/v1/services", {
method: "POST",
body: formData,
});
Using curl
curl -X POST http://localhost:8000/api/v1/services \
-F "intent=execute" \
-F "service=documents" \
-F "action=upload" \
-F "title=Q4 Report" \
-F "document=@./report.pdf"
Action Handler
Your action handler receives a StructuredPayload with fields and files separated:
import { Ok, Err } from "slang-ts";
import { createAction, type Action } from "@nilejs/nile";
const uploadDocument: Action = createAction({
name: "upload",
description: "Upload a document",
isSpecial: {
contentType: "multipart/form-data",
uploadMode: "flat",
},
handler: (data, context) => {
const { fields, files } = data as {
fields: Record<string, string | string[]>;
files: Record<string, File | File[]>;
};
const title = fields.title as string;
const document = files.document as File;
// Process the file (save to storage, etc.)
return Ok({
title,
filename: document.name,
size: document.size,
type: document.type,
});
},
});
Action-Level Config
The isSpecial field on an action controls upload behavior:
Parsing Modes
Flat Mode (default)
Rejects requests where the same form-data key carries both files and string fields. This prevents ambiguous payloads.
✓ document=@file.pdf, title="Report" (different keys)
✗ data=@file.pdf, data="some string" (same key, mixed types)
Structured Mode
Allows any combination of keys. Files and fields are separated into their respective buckets, with duplicate keys aggregated into arrays.
✓ attachment=@file1.pdf, attachment=@file2.pdf (array of files)
✓ tag=frontend, tag=docs (array of strings)
Validation Chain
Every upload request passes through a 7-step validation chain that fails fast on the first error:
- Filename length — rejects files with names exceeding the configured limit
- Zero-byte detection — rejects empty files
- Minimum size — rejects files smaller than the threshold
- File count — rejects requests exceeding the max file count
- Per-file size — rejects individual files exceeding the size limit
- Total size — rejects requests where combined file size exceeds the limit
- MIME + extension allowlist — rejects files that don't match both the allowed MIME type and extension
Error Responses
Validation errors return structured error data with the error_category field:
{
"status": false,
"message": "upload limit exceeded",
"data": {
"error_category": "validation",
"limit": "maxFileSize",
"max": 5242880,
"files": [{ "name": "huge-video.mp4", "size": 104857600 }]
}
}
{
"status": false,
"message": "file type not allowed",
"data": {
"error_category": "validation",
"rejected": [{ "name": "script.exe", "type": "application/x-msdownload" }],
"allowed": {
"mimeTypes": ["image/png", "image/jpeg", "application/pdf"],
"extensions": [".png", ".jpg", ".jpeg", ".pdf"]
}
}
}
{
"status": false,
"message": "mixed key types not allowed",
"data": {
"error_category": "validation",
"conflicts": ["data"],
"hint": "Same key cannot be used for both files and fields"
}
}
Content-Type Enforcement
When enforceContentType is enabled and an action specifies isSpecial.contentType, Nile checks that the incoming request's content type matches. Mismatches return 415 Unsupported Media Type.
This is useful when certain actions should only accept file uploads and reject JSON requests.