Storage & Uploads
File Upload
File Upload — Storage & Uploads documentation for BCP Framework.
Last updated: 2026-08-29T01:11:41
File Upload
BCP Framework provides two server-only multipart upload paths through bcp/server:
- the buffered
FormDatahelpers introduced in0.1.24, - direct multipart-to-storage streaming added in
0.1.26.
Use the buffered API for small forms and workflows that need normal FormData access. Use storeMultipartFile() when large files should flow directly from the request body into a StorageAdapter without materializing the complete file in application memory.
Buffered API route
import {
parseMultipartFormData,
requireUploadedFile,
saveUploadedFile,
} from "bcp/server";
export async function POST(
request: Request
) {
const formData =
await parseMultipartFormData(
request,
{
maxBytes:
8 * 1024 * 1024,
}
);
const file =
requireUploadedFile(
formData,
"file",
{
maxBytes:
5 * 1024 * 1024,
allowedTypes: [
"image/png",
"image/jpeg",
"image/webp",
],
allowedExtensions: [
".png",
".jpg",
".jpeg",
".webp",
],
}
);
const saved =
await saveUploadedFile(
file,
{
directory:
"./uploads",
}
);
return Response.json({
fileName:
saved.fileName,
size:
saved.size,
checksumSha256:
saved.checksumSha256,
});
}Production streaming upload
BCP 0.1.26 adds storeMultipartFile():
import {
createLocalStorage,
storeMultipartFile,
} from "bcp/server";
const uploads =
createLocalStorage({
directory:
"./uploads",
});
export async function POST(
request: Request
) {
const stored =
await storeMultipartFile(
request,
{
storage:
uploads,
fieldName:
"file",
key:
"incoming/upload.bin",
maxBytes:
100 * 1024 * 1024,
constraints: {
maxBytes:
80 * 1024 * 1024,
allowedTypes: [
"application/octet-stream",
],
},
}
);
return Response.json(
stored
);
}The request body is parsed incrementally. The target file stream is handed to the selected StorageAdapter while multipart data is still arriving.
This path is intended for large uploads and shared/object storage. It avoids calling request.formData() for the uploaded file.
Streaming directly to S3-compatible storage
The same API works with createS3Storage():
import {
createS3Storage,
storeMultipartFile,
} from "bcp/server";
const uploads =
createS3Storage({
bucket:
process.env.S3_BUCKET!,
region:
process.env.S3_REGION!,
endpoint:
process.env.S3_ENDPOINT,
accessKeyId:
process.env.S3_ACCESS_KEY_ID,
secretAccessKey:
process.env.S3_SECRET_ACCESS_KEY,
});
export async function POST(
request: Request
) {
return Response.json(
await storeMultipartFile(
request,
{
storage:
uploads,
fieldName:
"file",
key: (
file
) =>
`documents/${crypto.randomUUID()}${file.originalName.endsWith(".pdf") ? ".pdf" : ""}`,
constraints: {
maxBytes:
50 * 1024 * 1024,
allowedTypes: [
"application/pdf",
],
allowedExtensions: [
".pdf",
],
},
}
)
);
}The key option can be a fixed string or an async callback receiving:
{
fieldName,
originalName,
encoding,
contentType,
}When key is omitted, BCP creates a UUID-based key and preserves a short sanitized extension.
Request body limit
BCP's security gateway enforces the framework-wide request body limit before the request reaches the application server.
The default is 1 MiB. Applications that accept larger uploads must explicitly raise it:
// bcp.config.ts
import {
defineConfig,
} from "bcp/config";
export default defineConfig({
server: {
bodyLimit:
100 * 1024 * 1024,
},
});The same setting can be supplied with BCP_BODY_LIMIT.
For buffered uploads, keep server.bodyLimit greater than or equal to the multipart limit used by parseMultipartFormData().
For streaming uploads, keep the infrastructure/framework body limit greater than or equal to storeMultipartFile({ maxBytes }). Streaming avoids full application-memory buffering, but it does not bypass reverse-proxy, framework or platform request-size limits.
Multipart request limits
Buffered parsing:
const formData =
await parseMultipartFormData(
request,
{
maxBytes:
10 * 1024 * 1024,
}
);Streaming parsing:
await storeMultipartFile(
request,
{
storage,
fieldName:
"file",
maxBytes:
100 * 1024 * 1024,
}
);Both helpers:
- require
multipart/form-data, - reject a declared
Content-Lengthabove the configured request limit, - enforce the request limit while processing,
- throw
UploadErrorwith HTTP-oriented status information on validation failures.
storeMultipartFile() additionally applies the file-level constraints.maxBytes while bytes are flowing into storage. Native streaming adapters can abort incomplete writes rather than keeping a partial object.
Reading buffered files
Use getUploadedFile() for optional files:
const avatar =
getUploadedFile(
formData,
"avatar"
);
if (!avatar) {
// no file supplied
}Use requireUploadedFile() when a file is mandatory:
const avatar =
requireUploadedFile(
formData,
"avatar"
);A missing required file throws UploadError with code FILE_REQUIRED and status 400.
File constraints
Buffered and streaming uploads can validate:
{
maxBytes: 5 * 1024 * 1024,
allowedTypes: [
"application/pdf",
],
allowedExtensions: [
".pdf",
],
}Supported checks are:
- maximum file size,
- MIME type allowlist,
- extension allowlist.
MIME type and file extension are multipart metadata. They are useful validation signals but are not content-signature verification. Applications accepting security-sensitive formats should inspect the file content or magic bytes before trusting the file type.
Saving buffered files
const saved =
await saveUploadedFile(
file,
{
directory:
"./uploads/documents",
}
);When fileName is omitted, BCP generates a UUID-based storage name while preserving a short sanitized extension.
The result contains:
{
originalName: string;
fileName: string;
path: string;
type: string;
size: number;
checksumSha256: string;
}Files are created with exclusive-write behavior by default. An existing destination produces FILE_EXISTS instead of silently overwriting data.
Use overwrite: true only when replacement is intentional.
Streaming result
storeMultipartFile() returns storage metadata together with multipart identity:
{
key: string;
size: number;
contentType: string;
lastModified: Date;
etag: string;
checksumSha256?: string;
fieldName: string;
originalName: string;
encoding: string;
}The checksum is optional because not every storage provider exposes a portable whole-object SHA-256 value for multipart uploads.
File-name safety
saveUploadedFile() sanitizes the final local file name and verifies that the resolved destination remains inside the configured directory.
You can use the same sanitizer independently:
import {
sanitizeUploadFileName,
} from "bcp/server";
const safeName =
sanitizeUploadFileName(
userFileName
);Streaming storage uses logical storage keys instead of direct operating-system paths. Storage adapters validate their own key rules; local/S3 BCP adapters reject traversal segments.
Upload errors
import {
UploadError,
} from "bcp/server";
try {
// upload work
} catch (error) {
if (
error instanceof UploadError
) {
return Response.json(
{
error: {
code:
error.code,
message:
error.message,
},
},
{
status:
error.status,
}
);
}
throw error;
}Current error codes include:
INVALID_MULTIPART
UPLOAD_TOO_LARGE
FILE_REQUIRED
FILE_TOO_LARGE
FILE_TYPE_NOT_ALLOWED
FILE_EXTENSION_NOT_ALLOWED
INVALID_FILE_NAME
FILE_EXISTSThe streaming helper maps storage STREAM_TOO_LARGE and OBJECT_EXISTS failures into the corresponding upload-facing error codes.
Choosing buffered vs streaming
Prefer the buffered API when:
- uploads are small,
- the route needs several regular form fields/files as
FormData, - application code needs random access to the parsed file before storage.
Prefer storeMultipartFile() when:
- files can be large,
- the file should be written directly to local/shared/object storage,
- memory usage should stay bounded,
- S3 multipart upload is required for streams with unknown final size.
The 0.1.26 streaming API intentionally targets one named file field per call. Multiple-file orchestration and richer streaming form-field APIs can be added later without changing the storage contract.