Overview
The AdonisJS Starter Kit uses @jrmc/adonis-attachment for handling file uploads and attachments. This package provides a clean, type-safe way to manage file uploads with support for multiple storage drivers and automatic file variants (like thumbnails).
Features
Multiple storage drivers (local, S3, etc.)
Automatic file variants and image processing
Type-safe attachment handling
Pre-computed URLs for better performance
Seamless integration with Lucid models
Configuration
The attachment configuration is located in config/attachment.ts:
import { defineConfig } from '@jrmc/adonis-attachment'
import { InferConverters } from '@jrmc/adonis-attachment/types/config'
const attachmentConfig = defineConfig ({
converters: {
thumbnail: {
options: {
resize: 300 ,
format: 'webp' ,
},
},
},
})
export default attachmentConfig
declare module '@jrmc/adonis-attachment' {
interface AttachmentVariants extends InferConverters < typeof attachmentConfig > {}
}
The configuration defines image variants that are automatically generated. In this example, a thumbnail variant is created by resizing images to 300px and converting to WebP format.
Model Integration
Adding Attachments to Models
Here’s how the User model implements avatar attachments:
import { attachment , attachmentManager } from '@jrmc/adonis-attachment'
import type { Attachment } from '@jrmc/adonis-attachment/types/attachment'
import BaseModel from '#common/models/base_model'
export default class User extends BaseModel {
// ... other columns
@ attachment ({ preComputeUrl: false , variants: [ 'thumbnail' ] })
declare avatar : Attachment
@ column ()
declare avatarUrl : string | null
static async preComputeUrls ( models : User | User []) {
if ( Array . isArray ( models )) {
await Promise . all ( models . map (( model ) => this . preComputeUrls ( model )))
return
}
if ( ! models . avatar ) {
return
}
const thumbnail = models . avatar . getVariant ( 'thumbnail' )
if ( thumbnail ) {
await attachmentManager . computeUrl ( thumbnail )
}
}
}
The @attachment decorator options:
preComputeUrl: false - URLs are computed on-demand instead of automatically
variants: ['thumbnail'] - Specifies which variants to generate (must match config)
Pre-computing URLs
The preComputeUrls method optimizes performance by generating signed URLs before rendering:
// Pre-compute URLs before sending to the frontend
await User . preComputeUrls ( users )
// Or for a single user
await User . preComputeUrls ( auth . user ! )
Controller Implementation
Here’s how file uploads are handled in the ProfileController:
app/users/controllers/profile_controller.ts
import type { HttpContext } from '@adonisjs/core/http'
import { attachmentManager } from '@jrmc/adonis-attachment'
import User from '#users/models/user'
import { updateProfileValidator } from '#users/validators'
export default class ProfileController {
public async handle ({ auth , request , response } : HttpContext ) {
const { avatar , ... payload } = await request . validateUsing ( updateProfileValidator )
const user = await User . findOrFail ( auth . user ! . id )
if ( avatar ) {
user . avatar = await attachmentManager . createFromFile ( avatar )
}
user . merge ({
... payload ,
})
await user . save ()
return response . redirect (). toRoute ( 'profile.show' )
}
}
Extract file from request
The validator extracts the avatar file from the request payload
Create attachment
Use attachmentManager.createFromFile() to create an attachment from the uploaded file
Assign to model
Assign the attachment to the model property
Save model
Save the model - the attachment is automatically persisted
Validation
Validate file uploads using VineJS validators:
import vine from '@vinejs/vine'
export const updateProfileValidator = vine . compile (
vine . object ({
fullName: vine . string (). trim (). minLength ( 3 ). maxLength ( 255 ),
avatar: vine
. file ({
extnames: [ 'png' , 'jpg' , 'jpeg' , 'gif' ],
size: 1 * 1024 * 1024 , // 1MB
})
. nullable (),
})
)
Always validate file uploads to prevent security issues:
Restrict allowed file extensions
Set maximum file size limits
Consider scanning for malware in production
Frontend Implementation
import { useForm } from '@inertiajs/react'
function ProfileForm () {
const { data , setData , post , processing } = useForm ({
fullName: '' ,
avatar: null ,
})
const handleSubmit = ( e : React . FormEvent ) => {
e . preventDefault ()
post ( '/settings/profile' )
}
return (
< form onSubmit = { handleSubmit } >
< input
type = "text"
value = { data . fullName }
onChange = { ( e ) => setData ( 'fullName' , e . target . value ) }
/>
< input
type = "file"
accept = "image/*"
onChange = { ( e ) => setData ( 'avatar' , e . target . files [ 0 ]) }
/>
< button type = "submit" disabled = { processing } >
Update Profile
</ button >
</ form >
)
}
Displaying Attachments
function UserAvatar ({ user }) {
// Access the thumbnail variant
const avatarUrl = user . avatar ?. variants ?. thumbnail ?. url
return avatarUrl ? (
< img src = { avatarUrl } alt = { user . fullName } />
) : (
< div > No avatar </ div >
)
}
Storage Drivers
Local Storage (Default)
By default, files are stored locally. Configure the storage path in your environment:
S3 Storage
To use S3 for file storage:
Install the S3 driver:
pnpm add @adonisjs/drive-s3
Configure S3 credentials in .env:
DRIVE_DISK=s3
S3_KEY=your-key
S3_SECRET=your-secret
S3_BUCKET=your-bucket
S3_REGION=us-east-1
Advanced Usage
Multiple Variants
Define multiple image variants for different use cases:
const attachmentConfig = defineConfig ({
converters: {
thumbnail: {
options: {
resize: 300 ,
format: 'webp' ,
},
},
medium: {
options: {
resize: 800 ,
format: 'webp' ,
},
},
large: {
options: {
resize: 1920 ,
format: 'webp' ,
},
},
},
})
Deleting Attachments
// Delete the attachment and its variants
if ( user . avatar ) {
await attachmentManager . delete ( user . avatar )
user . avatar = null
await user . save ()
}
Custom File Names
const attachment = await attachmentManager . createFromFile ( file , {
name: 'custom-name' ,
})
Best Practices
Pre-compute URLs before sending data to the frontend to avoid N+1 query problems: const users = await User . query (). paginate ( 1 , 10 )
await User . preComputeUrls ( users )
Create variants for different use cases (thumbnails for lists, medium for previews, large for detail views) to optimize bandwidth and loading times.
Always handle upload errors gracefully and provide user feedback: try {
user . avatar = await attachmentManager . createFromFile ( avatar )
await user . save ()
} catch ( error ) {
return response . badRequest ({ message: 'Failed to upload file' })
}
When replacing attachments, delete old files to prevent storage bloat: if ( user . avatar ) {
await attachmentManager . delete ( user . avatar )
}
user . avatar = await attachmentManager . createFromFile ( newAvatar )
Next Steps
Models Learn more about model configuration and relationships
Validation Explore validation patterns and best practices