Version: 1.0.0
This document specifies how to handle file uploads, image uploads, and attachment field types in ObjectQL APIs.
π‘ Quick Guides:
Overview
Field Types
Data Format
Upload API
CRUD Operations with Attachments
Download & Access
Best Practices
Examples
ObjectQL supports two attachment-related field types:
file : General file attachments (documents, PDFs, archives, etc.)
image : Image files with optional image-specific metadata (including user avatars, product photos, galleries, etc.)
All attachment fields store metadata as JSON in the database, while the actual file content is stored in a configurable storage backend (local filesystem, S3, cloud storage, etc.).
Note: User profile pictures (avatars) should use the image type with appropriate constraints (e.g., multiple: false, size limits, aspect ratio requirements). UI frameworks can identify avatar fields by naming conventions (e.g., profile_picture, avatar) to apply specific rendering (circular cropping, etc.).
Metadata-Driven : File metadata (URL, size, type) is stored in the database
Storage-Agnostic : Actual files can be stored anywhere (local, S3, CDN)
URL-Based : Files are referenced by URLs for maximum flexibility
Type-Safe : Full TypeScript support for attachment data structures
Validation : Built-in file type, size, and extension validation
General-purpose file attachment field.
Properties:
Property Type Description type'file'Required. Field type identifierlabelstringDisplay label requiredbooleanWhether file is mandatory multiplebooleanAllow multiple file uploads (array) acceptstring[]Allowed file extensions (e.g., ['.pdf', '.docx']) max_sizenumberMaximum file size in bytes min_sizenumberMinimum file size in bytes
Example Definition:
# expense.object.yml
fields :
receipt :
type : file
label : Receipt Attachment
required : true
accept : [ '.pdf' , '.jpg' , '.png' ]
max_size : 5242880 # 5MB
supporting_docs :
type : file
label : Supporting Documents
multiple : true
accept : [ '.pdf' , '.docx' , '.xlsx' ]
max_size : 10485760 # 10MB
Image-specific attachment field with additional metadata support.
Properties:
Property Type Description type'image'Required. Field type identifierlabelstringDisplay label requiredbooleanWhether image is mandatory multiplebooleanAllow multiple images (gallery) acceptstring[]Allowed image formats (default: ['.jpg', '.jpeg', '.png', '.gif', '.webp']) max_sizenumberMaximum file size in bytes max_widthnumberMaximum image width in pixels max_heightnumberMaximum image height in pixels min_widthnumberMinimum image width in pixels min_heightnumberMinimum image height in pixels
Example Definition:
# product.object.yml
fields :
product_image :
type : image
label : Product Image
required : true
accept : [ '.jpg' , '.png' , '.webp' ]
max_size : 2097152 # 2MB
max_width : 2000
max_height : 2000
gallery :
type : image
label : Product Gallery
multiple : true
max_size : 5242880 # 5MB per image
# User avatar (profile picture)
profile_picture :
type : image
label : Profile Picture
multiple : false # Single image only
max_size : 1048576 # 1MB
max_width : 500
max_height : 500
accept : [ '.jpg' , '.png' , '.webp' ]
Attachment fields store structured JSON data containing file metadata. The actual file content is stored separately.
For non-multiple file/image fields, the data is stored as a single object:
interface AttachmentData {
/** Unique identifier for this file */
id ?: string ;
/** File name (e.g., "invoice.pdf") */
name : string ;
/** Publicly accessible URL to the file */
url : string ;
/** File size in bytes */
size : number ;
/** MIME type (e.g., "application/pdf", "image/jpeg") */
type : string ;
/** Original filename as uploaded by user */
original_name ?: string ;
/** Upload timestamp (ISO 8601) */
uploaded_at ?: string ;
/** User ID who uploaded the file */
uploaded_by ?: string ;
}
Example:
{
"id" : "file_abc123" ,
"name" : "receipt_2024.pdf" ,
"url" : "https://cdn.example.com/files/receipt_2024.pdf" ,
"size" : 245760 ,
"type" : "application/pdf" ,
"original_name" : "Receipt - Jan 2024.pdf" ,
"uploaded_at" : "2024-01-15T10:30:00Z" ,
"uploaded_by" : "user_xyz"
}
For multiple: true fields, the data is an array of attachment objects:
type MultipleAttachmentData = AttachmentData [];
Example:
[
{
"id" : "img_001" ,
"name" : "product_front.jpg" ,
"url" : "https://cdn.example.com/images/product_front.jpg" ,
"size" : 156789 ,
"type" : "image/jpeg" ,
"uploaded_at" : "2024-01-15T10:30:00Z"
},
{
"id" : "img_002" ,
"name" : "product_back.jpg" ,
"url" : "https://cdn.example.com/images/product_back.jpg" ,
"size" : 142356 ,
"type" : "image/jpeg" ,
"uploaded_at" : "2024-01-15T10:31:00Z"
}
]
Image fields can include additional metadata:
interface ImageAttachmentData extends AttachmentData {
/** Image width in pixels */
width ?: number ;
/** Image height in pixels */
height ?: number ;
/** Thumbnail URL (if generated) */
thumbnail_url ?: string ;
/** Alternative sizes/versions */
variants ?: {
small ?: string ;
medium ?: string ;
large ?: string ;
};
}
Example:
{
"id" : "img_abc123" ,
"name" : "product_hero.jpg" ,
"url" : "https://cdn.example.com/images/product_hero.jpg" ,
"size" : 523400 ,
"type" : "image/jpeg" ,
"width" : 1920 ,
"height" : 1080 ,
"thumbnail_url" : "https://cdn.example.com/images/product_hero_thumb.jpg" ,
"variants" : {
"small" : "https://cdn.example.com/images/product_hero_small.jpg" ,
"medium" : "https://cdn.example.com/images/product_hero_medium.jpg" ,
"large" : "https://cdn.example.com/images/product_hero_large.jpg"
}
}
ObjectQL provides dedicated endpoints for file uploads using multipart/form-data.
POST /api/files/upload
Content-Type: multipart/form-data
Authorization: Bearer <token>
Use standard multipart form data with the following fields:
Field Type Required Description fileFile Yes The file to upload objectstring No Object name (for context/validation) fieldstring No Field name (for validation against field config) folderstring No Logical folder/path for organization
Success Response (200 OK):
{
"data" : {
"id" : "file_abc123" ,
"name" : "invoice.pdf" ,
"url" : "https://cdn.example.com/files/invoice.pdf" ,
"size" : 245760 ,
"type" : "application/pdf" ,
"uploaded_at" : "2024-01-15T10:30:00Z" ,
"uploaded_by" : "user_xyz"
}
}
Error Response (400 Bad Request):
{
"error" : {
"code" : "FILE_VALIDATION_ERROR" ,
"message" : "File validation failed" ,
"details" : {
"file" : "invoice.exe" ,
"reason" : "File type not allowed. Allowed types: .pdf, .jpg, .png"
}
}
}
Using cURL:
curl -X POST https://api.example.com/api/files/upload \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-F "file=@/path/to/invoice.pdf" \
-F "object=expense" \
-F "field=receipt"
Using JavaScript Fetch:
const fileInput = document. getElementById ( 'fileInput' );
const file = fileInput.files[ 0 ];
const formData = new FormData ();
formData. append ( 'file' , file);
formData. append ( 'object' , 'expense' );
formData. append ( 'field' , 'receipt' );
const response = await fetch ( '/api/files/upload' , {
method: 'POST' ,
headers: {
'Authorization' : 'Bearer ' + token
},
body: formData
});
const { data : uploadedFile } = await response. json ();
console. log ( 'Uploaded:' , uploadedFile);
// { id: 'file_abc123', url: '...', ... }
Using Axios:
import axios from 'axios' ;
const formData = new FormData ();
formData. append ( 'file' , file);
formData. append ( 'object' , 'expense' );
formData. append ( 'field' , 'receipt' );
const response = await axios. post ( '/api/files/upload' , formData, {
headers: {
'Content-Type' : 'multipart/form-data' ,
'Authorization' : 'Bearer ' + token
}
});
const uploadedFile = response.data.data;
For uploading multiple files at once:
POST /api/files/upload/batch
Content-Type: multipart/form-data
Request:
curl -X POST https://api.example.com/api/files/upload/batch \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-F "files=@/path/to/image1.jpg" \
-F "files=@/path/to/image2.jpg" \
-F "files=@/path/to/image3.jpg" \
-F "object=product" \
-F "field=gallery"
Response:
{
"data" : [
{
"id" : "img_001" ,
"name" : "image1.jpg" ,
"url" : "https://cdn.example.com/images/image1.jpg" ,
"size" : 156789 ,
"type" : "image/jpeg"
},
{
"id" : "img_002" ,
"name" : "image2.jpg" ,
"url" : "https://cdn.example.com/images/image2.jpg" ,
"size" : 142356 ,
"type" : "image/jpeg"
},
{
"id" : "img_003" ,
"name" : "image3.jpg" ,
"url" : "https://cdn.example.com/images/image3.jpg" ,
"size" : 198234 ,
"type" : "image/jpeg"
}
]
}
Step 1: Upload the file(s)
// Upload file first
const uploadResponse = await fetch ( '/api/files/upload' , {
method: 'POST' ,
headers: { 'Authorization' : 'Bearer ' + token },
body: formData
});
const uploadedFile = ( await uploadResponse. json ()).data;
Step 2: Create record with file metadata
// Create expense record with the uploaded file
const createResponse = await fetch ( '/api/objectql' , {
method: 'POST' ,
headers: {
'Content-Type' : 'application/json' ,
'Authorization' : 'Bearer ' + token
},
body: JSON . stringify ({
op: 'create' ,
object: 'expense' ,
args: {
expense_number: 'EXP-2024-001' ,
amount: 125.50 ,
description: 'Office supplies' ,
receipt: uploadedFile // Reference to uploaded file
}
})
});
const expense = ( await createResponse. json ()).data;
Complete Example (JSON-RPC):
{
"op" : "create" ,
"object" : "expense" ,
"args" : {
"expense_number" : "EXP-2024-001" ,
"amount" : 125.50 ,
"category" : "office_supplies" ,
"description" : "Office supplies - printer paper" ,
"receipt" : {
"id" : "file_abc123" ,
"name" : "receipt.pdf" ,
"url" : "https://cdn.example.com/files/receipt.pdf" ,
"size" : 245760 ,
"type" : "application/pdf"
}
}
}
{
"op" : "create" ,
"object" : "product" ,
"args" : {
"name" : "Premium Laptop" ,
"price" : 1299.99 ,
"description" : "High-performance laptop" ,
"gallery" : [
{
"id" : "img_001" ,
"name" : "laptop_front.jpg" ,
"url" : "https://cdn.example.com/images/laptop_front.jpg" ,
"size" : 156789 ,
"type" : "image/jpeg" ,
"width" : 1920 ,
"height" : 1080
},
{
"id" : "img_002" ,
"name" : "laptop_back.jpg" ,
"url" : "https://cdn.example.com/images/laptop_back.jpg" ,
"size" : 142356 ,
"type" : "image/jpeg" ,
"width" : 1920 ,
"height" : 1080
}
]
}
}
Replace entire attachment:
{
"op" : "update" ,
"object" : "expense" ,
"args" : {
"id" : "exp_xyz" ,
"data" : {
"receipt" : {
"id" : "file_new123" ,
"name" : "updated_receipt.pdf" ,
"url" : "https://cdn.example.com/files/updated_receipt.pdf" ,
"size" : 198234 ,
"type" : "application/pdf"
}
}
}
}
Add to multiple attachments (array):
// First, fetch the current record
const currentRecord = await fetch ( '/api/objectql' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({
op: 'findOne' ,
object: 'product' ,
args: 'product_123'
})
}). then ( r => r. json ());
// Upload new image
const newImage = await uploadFile (file);
// Update with appended gallery
await fetch ( '/api/objectql' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({
op: 'update' ,
object: 'product' ,
args: {
id: 'product_123' ,
data: {
gallery: [ ... currentRecord.data.gallery, newImage]
}
}
})
});
Remove attachment:
{
"op" : "update" ,
"object" : "expense" ,
"args" : {
"id" : "exp_xyz" ,
"data" : {
"receipt" : null
}
}
}
Attachments are returned as part of the record data:
{
"op" : "find" ,
"object" : "expense" ,
"args" : {
"fields" : [ "id" , "expense_number" , "amount" , "receipt" ],
"filters" : [[ "status" , "=" , "approved" ]]
}
}
Response:
{
"data" : [
{
"id" : "exp_001" ,
"expense_number" : "EXP-2024-001" ,
"amount" : 125.50 ,
"receipt" : {
"id" : "file_abc123" ,
"name" : "receipt.pdf" ,
"url" : "https://cdn.example.com/files/receipt.pdf" ,
"size" : 245760 ,
"type" : "application/pdf"
}
},
{
"id" : "exp_002" ,
"expense_number" : "EXP-2024-002" ,
"amount" : 89.99 ,
"receipt" : null // No receipt attached
}
]
}
Check if a file is attached:
{
"op" : "find" ,
"object" : "expense" ,
"args" : {
"filters" : [[ "receipt" , "!=" , null ]]
}
}
Check if no file is attached:
{
"op" : "find" ,
"object" : "expense" ,
"args" : {
"filters" : [[ "receipt" , "=" , null ]]
}
}
Files are accessed directly via their url property:
const expense = await fetchExpense ( 'exp_123' );
const receiptUrl = expense.receipt.url;
// Download file
window. open (receiptUrl, '_blank' );
// Or display image
document. getElementById ( 'receipt-img' ).src = receiptUrl;
For files requiring authentication:
GET /api/files/:fileId
Authorization: Bearer <token>
Example:
curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \
https://api.example.com/api/files/file_abc123 \
--output receipt.pdf
For images, request specific sizes:
GET /api/files/:fileId/thumbnail?size=small|medium|large
GET /api/files/:fileId/preview?width=200&height=200
Example:
<!-- Display thumbnail -->
< img src = "/api/files/img_abc123/thumbnail?size=medium" alt = "Product" />
<!-- Display custom size -->
< img src = "/api/files/img_abc123/preview?width=300&height=300" alt = "Product" />
Validate File Types : Always specify accept to restrict file types
Enforce Size Limits : Set appropriate max_size to prevent abuse
Scan for Malware : Integrate virus scanning for uploaded files
Use Signed URLs : For sensitive files, use time-limited signed URLs
Authenticate Downloads : Require authentication for private files
Use CDN : Store files on CDN for fast global access
Generate Thumbnails : Pre-generate image thumbnails for galleries
Lazy Load Images : Load images on-demand in lists
Compress Images : Automatically compress uploaded images
Cache Metadata : Cache file metadata to reduce database queries
Organize by Object : Store files in folders by object type
Use Object Storage : Use S3, GCS, or Azure Blob for scalability
Implement Cleanup : Delete orphaned files periodically
Version Files : Keep file versions for audit trails
Backup Regularly : Include files in backup strategy
Show Upload Progress : Display progress bars for large files
Preview Before Upload : Show image previews before submission
Validate Client-Side : Check file type/size before upload
Provide Feedback : Clear error messages for upload failures
Support Drag & Drop : Enable drag-and-drop file upload
/**
* Upload a file and create an expense record
*/
async function createExpenseWithReceipt ( expenseData , receiptFile ) {
// Step 1: Upload the receipt file
const formData = new FormData ();
formData. append ( 'file' , receiptFile);
formData. append ( 'object' , 'expense' );
formData. append ( 'field' , 'receipt' );
const uploadResponse = await fetch ( '/api/files/upload' , {
method: 'POST' ,
headers: {
'Authorization' : 'Bearer ' + getAuthToken ()
},
body: formData
});
if ( ! uploadResponse.ok) {
throw new Error ( 'File upload failed' );
}
const uploadedFile = ( await uploadResponse. json ()).data;
// Step 2: Create expense record with file metadata
const createResponse = await fetch ( '/api/objectql' , {
method: 'POST' ,
headers: {
'Content-Type' : 'application/json' ,
'Authorization' : 'Bearer ' + getAuthToken ()
},
body: JSON . stringify ({
op: 'create' ,
object: 'expense' ,
args: {
... expenseData,
receipt: uploadedFile
}
})
});
if ( ! createResponse.ok) {
throw new Error ( 'Failed to create expense' );
}
return ( await createResponse. json ()).data;
}
// Usage
const file = document. getElementById ( 'receipt-input' ).files[ 0 ];
const expense = await createExpenseWithReceipt ({
expense_number: 'EXP-2024-001' ,
amount: 125.50 ,
category: 'office_supplies' ,
description: 'Printer paper and toner'
}, file);
console. log ( 'Created expense:' , expense);
/**
* Upload multiple images and create product
*/
async function createProductWithGallery ( productData , imageFiles ) {
// Upload all images
const uploadPromises = Array. from (imageFiles). map ( async ( file ) => {
const formData = new FormData ();
formData. append ( 'file' , file);
formData. append ( 'object' , 'product' );
formData. append ( 'field' , 'gallery' );
const response = await fetch ( '/api/files/upload' , {
method: 'POST' ,
headers: { 'Authorization' : 'Bearer ' + getAuthToken () },
body: formData
});
return ( await response. json ()).data;
});
const uploadedImages = await Promise . all (uploadPromises);
// Create product with gallery
const response = await fetch ( '/api/objectql' , {
method: 'POST' ,
headers: {
'Content-Type' : 'application/json' ,
'Authorization' : 'Bearer ' + getAuthToken ()
},
body: JSON . stringify ({
op: 'create' ,
object: 'product' ,
args: {
... productData,
gallery: uploadedImages
}
})
});
return ( await response. json ()).data;
}
// Usage
const files = document. getElementById ( 'gallery-input' ).files;
const product = await createProductWithGallery ({
name: 'Premium Laptop' ,
price: 1299.99 ,
description: 'High-performance laptop'
}, files);
/**
* Update user profile picture
*/
async function updateUserAvatar ( userId , avatarFile ) {
// Upload avatar
const formData = new FormData ();
formData. append ( 'file' , avatarFile);
formData. append ( 'object' , 'user' );
formData. append ( 'field' , 'profile_picture' );
const uploadResponse = await fetch ( '/api/files/upload' , {
method: 'POST' ,
headers: { 'Authorization' : 'Bearer ' + getAuthToken () },
body: formData
});
const uploadedAvatar = ( await uploadResponse. json ()).data;
// Update user record
const updateResponse = await fetch ( '/api/objectql' , {
method: 'POST' ,
headers: {
'Content-Type' : 'application/json' ,
'Authorization' : 'Bearer ' + getAuthToken ()
},
body: JSON . stringify ({
op: 'update' ,
object: 'user' ,
args: {
id: userId,
data: {
profile_picture: uploadedAvatar
}
}
})
});
return ( await updateResponse. json ()).data;
}
// Usage
const avatarFile = document. getElementById ( 'avatar-input' ).files[ 0 ];
const updatedUser = await updateUserAvatar ( 'user_123' , avatarFile);
import React, { useState } from 'react' ;
import { ObjectQLClient } from '@objectql/sdk' ;
interface UploadReceiptProps {
expenseId ?: string ;
onSuccess ?: ( expense : any ) => void ;
}
export const UploadReceipt : React . FC < UploadReceiptProps > = ({
expenseId,
onSuccess
}) => {
const [ uploading , setUploading ] = useState ( false );
const [ error , setError ] = useState < string | null >( null );
const handleFileChange = async ( e : React . ChangeEvent < HTMLInputElement >) => {
const file = e.target.files?.[ 0 ];
if ( ! file) return ;
// Validate file
if ( ! file.type. match ( / ^ (application \/ pdf | image \/ (jpeg | png)) $ / )) {
setError ( 'Only PDF, JPG, and PNG files are allowed' );
return ;
}
if (file.size > 5 * 1024 * 1024 ) {
setError ( 'File size must be less than 5MB' );
return ;
}
setUploading ( true );
setError ( null );
try {
// Upload file
const formData = new FormData ();
formData. append ( 'file' , file);
formData. append ( 'object' , 'expense' );
formData. append ( 'field' , 'receipt' );
const uploadResponse = await fetch ( '/api/files/upload' , {
method: 'POST' ,
headers: {
'Authorization' : 'Bearer ' + getAuthToken ()
},
body: formData
});
if ( ! uploadResponse.ok) {
throw new Error ( 'Upload failed' );
}
const uploadedFile = ( await uploadResponse. json ()).data;
// Update expense with receipt
const client = new ObjectQLClient ();
const expense = await client. update ( 'expense' , expenseId ! , {
receipt: uploadedFile
});
onSuccess ?.(expense);
} catch (err) {
setError (err instanceof Error ? err.message : 'Upload failed' );
} finally {
setUploading ( false );
}
};
return (
< div >
< input
type = "file"
accept = ".pdf,.jpg,.jpeg,.png"
onChange = {handleFileChange}
disabled = {uploading}
/>
{ uploading && < p > Uploading ...</ p >}
{ error && < p style = {{ color : 'red' }} > {error} </ p > }
</ div >
);
};
Code Description FILE_TOO_LARGEFile exceeds max_size limit FILE_TOO_SMALLFile is smaller than min_size FILE_TYPE_NOT_ALLOWEDFile extension not in accept list IMAGE_DIMENSIONS_INVALIDImage dimensions don't meet requirements UPLOAD_FAILEDGeneral upload failure STORAGE_QUOTA_EXCEEDEDStorage quota exceeded FILE_NOT_FOUNDRequested file doesn't exist FILE_ACCESS_DENIEDUser doesn't have permission to access file
/**
* Attachment field data structure
*/
export interface AttachmentData {
id ?: string ;
name : string ;
url : string ;
size : number ;
type : string ;
original_name ?: string ;
uploaded_at ?: string ;
uploaded_by ?: string ;
}
/**
* Image-specific attachment data
*/
export interface ImageAttachmentData extends AttachmentData {
width ?: number ;
height ?: number ;
thumbnail_url ?: string ;
variants ?: {
small ?: string ;
medium ?: string ;
large ?: string ;
};
}
/**
* Upload response
*/
export interface UploadResponse {
data : AttachmentData ;
}
/**
* Batch upload response
*/
export interface BatchUploadResponse {
data : AttachmentData [];
}
ObjectQL provides a flexible file storage abstraction that supports multiple backends.
import { createNodeHandler, LocalFileStorage } from '@objectql/server' ;
import { ObjectQL } from '@objectql/core' ;
import * as http from 'http' ;
const app = new ObjectQL ({ /* ... */ });
// Configure local file storage
const fileStorage = new LocalFileStorage ({
baseDir: './uploads' , // or process.env.OBJECTQL_UPLOAD_DIR
baseUrl: 'http://localhost:3000/api/files' // or process.env.OBJECTQL_BASE_URL
});
// Create HTTP handler with file storage
const handler = createNodeHandler (app, { fileStorage });
const server = http. createServer (handler);
server. listen ( 3000 );
import { MemoryFileStorage } from '@objectql/server' ;
const fileStorage = new MemoryFileStorage ({
baseUrl: 'http://localhost:3000/api/files'
});
const handler = createNodeHandler (app, { fileStorage });
You can implement custom storage backends by implementing the IFileStorage interface:
import { IFileStorage, AttachmentData, FileStorageOptions } from '@objectql/server' ;
class S3FileStorage implements IFileStorage {
async save (
file : Buffer ,
filename : string ,
mimeType : string ,
options ?: FileStorageOptions
) : Promise < AttachmentData > {
// Upload to S3
const key = `${ options ?. folder || 'uploads'}/${ Date . now () }-${ filename }` ;
await s3. putObject ({
Bucket: 'my-bucket' ,
Key: key,
Body: file,
ContentType: mimeType
}). promise ();
return {
id: key,
name: filename,
url: `https://my-bucket.s3.amazonaws.com/${ key }` ,
size: file. length ,
type: mimeType,
uploaded_at: new Date (). toISOString (),
uploaded_by: options?.userId
};
}
async get ( fileId : string ) : Promise < Buffer | null > {
// Download from S3
const result = await s3. getObject ({
Bucket: 'my-bucket' ,
Key: fileId
}). promise ();
return result.Body as Buffer ;
}
async delete ( fileId : string ) : Promise < boolean > {
// Delete from S3
await s3. deleteObject ({
Bucket: 'my-bucket' ,
Key: fileId
}). promise ();
return true ;
}
getPublicUrl ( fileId : string ) : string {
return `https://my-bucket.s3.amazonaws.com/${ fileId }` ;
}
}
// Use custom storage
const fileStorage = new S3FileStorage ();
const handler = createNodeHandler (app, { fileStorage });
For detailed S3 integration guide with complete implementation code, see:
The file upload/download functionality is automatically available when using createNodeHandler:
POST /api/files/upload - Single file upload
POST /api/files/upload/batch - Batch file upload
GET /api/files/:fileId - Download file
File validation is automatically enforced based on field configuration:
# expense.object.yml
fields :
receipt :
type : file
label : Receipt
accept : [ '.pdf' , '.jpg' , '.png' ] # Only these extensions
max_size : 5242880 # 5MB max
min_size : 1024 # 1KB min
Validation errors return standardized responses:
{
"error" : {
"code" : "FILE_TOO_LARGE" ,
"message" : "File size (6000000 bytes) exceeds maximum allowed size (5242880 bytes)" ,
"details" : {
"file" : "receipt.pdf" ,
"size" : 6000000 ,
"max_size" : 5242880
}
}
}
Configure file storage behavior using environment variables:
Variable Description Default OBJECTQL_UPLOAD_DIRDirectory for local file storage ./uploadsOBJECTQL_BASE_URLBase URL for file access http://localhost:3000/api/files
Last Updated : January 2026
API Version : 1.0.0