VueJS Articles
Solve Vue.js Component Overload with Parent-Child Patterns
Has your Vue component become a tangled mess? Too much template markup, too many responsibilities, and decreasing maintainability.
Well the solution is to use component composition with parent-child relationships that Vue provides.
In this tutorial, you'll learn to:
1. Refactor effectively breaking up monolithic components into focused children
2. Pass data gracefully using props to send data from parent to child
3. Handle child events by capturing custom events emitted by child components
4. Maintain clean data flow by establishing predictable communication between components
See a real-world example where a parent component delegates UI to a specialized Toolbar child, creating cleaner code and better separation of concerns.
Parent Component:
<template>
<div class="parent-component">
<h2>Evaluate Product</h2>
<Toolbar :message="message" @evaluate-product="evaluate" />
<div class="parent-data">
<p>Total Likes: {{ total.likes }}</p>
<p>Total Dislikes: {{ total.dislikes }}</p>
<p v-if="action">Last Action: {{ action }}</p>
</div>
</div>
</template>
<script>
import Toolbar from './Toolbar.vue';
export default {
name: 'EvaluateComponent',
components: {
Toolbar
},
data(){
return {
total: {
likes: 0,
dislikes: 0
},
message: '',
action: null,
messageTimeout: null
}
},
methods:{
evaluate(task) {
// Clear previous timeout
if (this.messageTimeout) {
clearTimeout(this.messageTimeout);
}
switch(task) {
case 'like':
this.total.likes++;
this.message = "Like incremented successfully!";
this.action = 'like';
break;
case 'dislike':
this.total.dislikes++;
this.message = "Dislike incremented successfully!";
this.action = 'dislike';
break;
}
// Auto-clear message after 3 seconds
this.messageTimeout = setTimeout(() => {
this.message = '';
}, 3000);
}
},
beforeDestroy() {
// Clean up timeout when component is destroyed
if (this.messageTimeout) {
clearTimeout(this.messageTimeout);
}
}
}
</script>
Child Component (Toolbar tag in the parent):
<template>
<div class="child-component">
<div class="message" v-if="messageSet">{{ message }}</div>
<button @click="performEvaluate('like')">Like</button>
<button @click="performEvaluate('dislike')">Dislike</button>
</div>
</template>
<script>
export default {
props: {
message: {
type: String,
default: ''
}
},
data() {
return {
// You can add data properties here if needed
}
},
emits: ['evaluate-product'],
computed: {
messageSet() {
return this.message.length > 0;
}
},
methods: {
performEvaluate(evaluation) {
this.$emit('evaluate-product', evaluation);
}
}
}
</script>
Format Date and Time in JavaScript with Date Object
This tutorial The JavaScript Date library is a built-in object that represents a single moment in time. It provides methods for creating, parsing, formatting, and manipulating dates and times. Dates are stored as the number of milliseconds since the Unix epoch (January 1, 1970, 00:00:00 UTC).
In this tutorial I will cover how to format date only, time only, and both date and time using these 3 methods from built-in object Date. A method to format a date. A method to format a time. And a method to format both date and time.
const date = new Date('2025-10-23 11:06:48');
date.toLocaleDateString('en-US');
date.toLocaleTimeString('en-US');
date.toLocaleString('en-US');
Also, I have included 2 handy references at the end of this tutorial. A reference for options and values available to define the format. Also, a reference that lists all available methods on the Javascript built-in Date object.
Date toLocaleDateString()
The toLocaleDateString() method formats the date only. Below the value of the date will be initiated as datetime (yyyy-mm-dd hh:mm:ss):
const date = new Date('2025-10-23 11:06:48');
Even though the Date object is initiated with a date and time string, only the date part of the string will be extracted. The default date.toLocaleDateString('en-US') returns the date formatted as mm/dd/yyyy. Also use other formatting options available. The options are referenced below.
const date = new Date('2025-10-23 11:06:48');
// Date only
date.toLocaleDateString('en-US');
// "10/23/2025"
date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
// "October 23, 2025"
Date toLocaleTimeString()
Target and format the time portion of the string using toLocaleTimeString(). Now time is extracted from the initial value set below…
const date = new Date('2025-10-23 11:06:48');
The toLocaleTimeString() method can be used with or without options. The default time is in 12 hour format (hh:mm:ss AM/PM). Options are listed below:
// Time only
date.toLocaleTimeString('en-US');
// "11:06:48 AM"
date.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
hour12: true
});
// "11:06 AM"
Date toLocaleString()
To format both the date and time use toLocaleString(). This method also accepts options to provide formatting instructions. Refer to the list below.
// Combined date and time
date.toLocaleString('en-US');
// "10/23/2025, 11:06:48 AM"
date.toLocaleString('en-US').replace(/,/g, '');
// "10/23/2025 11:06:48 AM"
Below are all the possible format options that can be passed to toLocaleString() for the en-US locale.
Option Reference Table toLocaleString()
|
Parameter |
Possible Values |
Description |
Example Output |
|
DATE COMPONENTS |
|||
|
year |
'numeric' |
Full year |
2025 |
|
'2-digit' |
Two-digit year |
25 |
|
|
month |
'numeric' |
Month number |
10 |
|
'2-digit' |
Two-digit month |
10 |
|
|
'long' |
Full month name |
October |
|
|
'short' |
Abbreviated month |
Oct |
|
|
'narrow' |
Single character |
O |
|
|
day |
'numeric' |
Day of month |
23 |
|
'2-digit' |
Two-digit day |
23 |
|
|
weekday |
'long' |
Full weekday name |
Thursday |
|
'short' |
Abbreviated weekday |
Thu |
|
|
'narrow' |
Single character |
T |
|
|
TIME COMPONENTS |
|||
|
hour |
'numeric' |
Hour |
11 |
|
'2-digit' |
Two-digit hour |
11 |
|
|
minute |
'numeric' |
Minute |
6 |
|
'2-digit' |
Two-digit minute |
06 |
|
|
second |
'numeric' |
Second |
48 |
|
'2-digit' |
Two-digit second |
48 |
|
|
fractionalSecondDigits |
1, 2, 3 |
Milliseconds digits |
48.123 |
|
TIME SETTINGS |
|||
|
hour12 |
true |
12-hour clock |
11:06 AM |
|
false |
24-hour clock |
11:06 |
|
|
timeZone |
'America/New_York' |
Specific timezone |
Adjusts time |
|
'UTC' |
UTC timezone |
||
|
'Europe/London' |
Other timezones |
||
|
timeZoneName |
'short' |
Abbreviated timezone |
EDT |
|
'long' |
Full timezone name |
Eastern Daylight Time |
|
|
'shortOffset' |
Short offset |
GMT-4 |
|
|
'longOffset' |
Long offset |
GMT-04:00 |
|
|
'shortGeneric' |
Generic short |
ET |
|
|
'longGeneric' |
Generic long |
Eastern Time |
|
|
OTHER OPTIONS |
|||
|
era |
'long' |
Full era name |
Anno Domini |
|
'short' |
Abbreviated era |
AD |
|
|
'narrow' |
Single character |
A |
|
|
dayPeriod |
'narrow' |
AM/PM format |
AM |
|
'short' |
AM/PM format |
AM |
|
|
'long' |
AM/PM format |
AM |
|
|
calendar |
'gregory' |
Gregorian calendar |
Default |
|
numberingSystem |
'latn' |
Latin digits |
0123456789 |
|
PRESET STYLES |
|||
|
dateStyle |
'full' |
Complete date |
Thursday, October 23, 2025 |
|
'long' |
Long format |
October 23, 2025 |
|
|
'medium' |
Medium format |
Oct 23, 2025 |
|
|
'short' |
Short format |
10/23/2025 |
|
|
timeStyle |
'full' |
Complete time |
11:06:48 AM Eastern Daylight Time |
|
'long' |
Long format |
11:06:48 AM EDT |
|
|
'medium' |
Medium format |
11:06:48 AM |
|
|
'short' |
Short format |
11:06 AM |
JavaScript Date Library Function Reference
Constructor Methods
|
Method |
Description |
Example |
|
new Date() |
Current date/time |
new Date() |
|
new Date(milliseconds) |
From milliseconds since epoch |
new Date(1634980000000) |
|
new Date(dateString) |
From ISO string |
new Date("2025-10-23") |
|
new Date(year, month, day, hours, minutes, seconds, ms) |
From components |
new Date(2025, 9, 23) |
Static Methods
|
Method |
Description |
Returns |
|
Date.now() |
Current timestamp in milliseconds |
1634980000000 |
|
Date.parse() |
Parse date string to milliseconds |
1634980000000 |
|
Date.UTC() |
UTC timestamp from components |
1634980000000 |
Getter Methods: Local Time Getters
|
Method |
Description |
Range |
|
getFullYear() |
4-digit year |
1900+ |
|
getMonth() |
Month |
0-11 |
|
getDate() |
Day of month |
1-31 |
|
getDay() |
Day of week |
0-6 |
|
getHours() |
Hours |
0-23 |
|
getMinutes() |
Minutes |
0-59 |
|
getSeconds() |
Seconds |
0-59 |
|
getMilliseconds() |
Milliseconds |
0-999 |
|
getTime() |
Milliseconds since epoch |
0+ |
|
getTimezoneOffset() |
Timezone offset in minutes |
-720 to 720 |
UTC Getters
|
Method |
Description |
Range |
|
getUTCFullYear() |
UTC 4-digit year |
1900+ |
|
getUTCMonth() |
UTC month |
0-11 |
|
getUTCDate() |
UTC day of month |
1-31 |
|
getUTCDay() |
UTC day of week |
0-6 |
|
getUTCHours() |
UTC hours |
0-23 |
|
getUTCMinutes() |
UTC minutes |
0-59 |
|
getUTCSeconds() |
UTC seconds |
0-59 |
|
getUTCMilliseconds() |
UTC milliseconds |
0-999 |
Setter Methods
Local Time Setters
|
Method |
Description |
Parameters |
|
setFullYear() |
Set year |
year[, month, day] |
|
setMonth() |
Set month |
month[, day] |
|
setDate() |
Set day of month |
day |
|
setHours() |
Set hours |
hours[, min, sec, ms] |
|
setMinutes() |
Set minutes |
minutes[, sec, ms] |
|
setSeconds() |
Set seconds |
seconds[, ms] |
|
setMilliseconds() |
Set milliseconds |
ms |
|
setTime() |
Set time via milliseconds |
milliseconds |
UTC Setters
|
Method |
Description |
Parameters |
|
setUTCFullYear() |
Set UTC year |
year[, month, day] |
|
setUTCMonth() |
Set UTC month |
month[, day] |
|
setUTCDate() |
Set UTC day of month |
day |
|
setUTCHours() |
Set UTC hours |
hours[, min, sec, ms] |
|
setUTCMinutes() |
Set UTC minutes |
minutes[, sec, ms] |
|
setUTCSeconds() |
Set UTC seconds |
seconds[, ms] |
|
setUTCMilliseconds() |
Set UTC milliseconds |
ms |
Conversion Methods
|
Method |
Description |
Example Output |
|
toString() |
Full date string |
"Thu Oct 23 2025 11:06:48 GMT-0400 (EDT)" |
|
toDateString() |
Date portion only |
"Thu Oct 23 2025" |
|
toTimeString() |
Time portion only |
"11:06:48 GMT-0400 (EDT)" |
|
toISOString() |
ISO 8601 format |
"2025-10-23T15:06:48.000Z" |
|
toUTCString() |
UTC string |
"Thu, 23 Oct 2025 15:06:48 GMT" |
|
toGMTString() |
GMT string (deprecated) |
"Thu, 23 Oct 2025 15:06:48 GMT" |
|
toJSON() |
JSON string |
"2025-10-23T15:06:48.000Z" |
|
toLocaleString() |
Locale date/time |
"10/23/2025, 11:06:48 AM" |
|
toLocaleDateString() |
Locale date only |
"10/23/2025" |
|
toLocaleTimeString() |
Locale time only |
"11:06:48 AM" |
Value Methods
|
Method |
Description |
Returns |
|
valueOf() |
Primitive value |
Milliseconds (same as getTime()) |
Laravel Webpack.mix Asset Compilation AND Performance Optimization
Getting Started with webpack.mix
This is an essential must know for Laravel developers. This tutorial will go through the basics of webpack.mix and preparing live CSS stylesheets and Javascript includes.
Locate webpack.mix.js and the /resources and /public directories. I will define a few ways to specify the files from resources compiled to the live public directory. Check out these 2 examples… they should cover necessary usage for 99% of projects.
Example: Combine multiple files into a single file.
In this example I’ll define a simple way to combine multiple custom javascript files into a single file. Let’s take 4 custom stylesheets and 4 javascript files. We will combine all specified stylesheets from /resources/css into a single stylesheet named /public/css/custom-all.css. Also let’s combine the specified javascript files from /resources/js into a single javascript file named /public/js/custom-all.js
All files that can be edited directly are located in the resources directory. All compiled files will be placed in the public directory where the code can be accessed live.
mix.js('resources/js/app.js', 'public/js')
.vue()
.sass('resources/sass/app.scss', 'public/css');
// Combine all custom JS into one file
mix.scripts([
'resources/js/custom/main.js',
'resources/js/custom/helpers.js',
'resources/js/custom/components/*.js'
], 'public/js/custom-all.js');
// Combine all custom CSS into one file
mix.styles([
'resources/css/custom/main.css',
'resources/css/custom/components/buttons.css',
'resources/css/custom/pages/*.css'
], 'public/css/custom-all.css');
Let’s see how to use things in a blade template:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Your Laravel Application</title>
<!-- Compiled CSS -->
<link href="{{ mix('css/app.css') }}" rel="stylesheet">
<link href="{{ mix('css/custom-all.css') }}" rel="stylesheet">
</head>
<body>
<div id="app">
<!-- Your application content -->
</div>
<!-- Compiled JavaScript -->
<script src="{{ mix('js/app.js') }}"></script>
<script src="{{ mix('js/custom-all.js') }}"></script>
</body>
</html>
Example: Prepare several files.
This example is very similar to the example above. The only difference is in this example we are generating individual files in the public directory for each file specified from the resources directory.
// Webpack-compiled JS and CSS
mix.js('resources/js/app.js', 'public/js')
.vue({ version: 2 })
.sass('resources/sass/app.scss', 'public/css') // Or .css() if not using Sass
.css('resources/css/app.css', 'public/css');
// Custom JS files (separate from Webpack bundle)
mix.js('resources/js/custom/main.js', 'public/js/custom.js')
.js('resources/js/custom/helpers.js', 'public/js/helpers.js');
// Custom CSS files (separate from Webpack bundle)
mix.css('resources/css/custom/main.css', 'public/css/custom.css')
.css('resources/css/custom/components/buttons.css', 'public/css/components/buttons.css');
Notice in webpack.mix.js the definitions for mix.js and mix.css methods. Each of the javascript files and stylesheets from the resources directory has a corresponding file in the public directory.
Now let’s look at how to use this in a blade template:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Your Laravel Application</title>
<!-- Compiled CSS -->
<link href="{{ mix('css/app.css') }}" rel="stylesheet">
<link href="{{ mix('css/custom.css') }}" rel="stylesheet">
<link href="{{ mix('css/components/buttons.css') }}" rel="stylesheet">
</head>
<body>
<div id="app">
<!-- Your application content -->
</div>
<!-- Compiled JavaScript -->
<script src="{{ mix('js/app.js') }}"></script>
<script src="{{ mix('js/custom.js') }}"></script>
<script src="{{ mix('js/helpers.js') }}"></script>
</body>
</html>
To generate the public CSS and JS files:
npm install
npm run dev
Scoped Slot in Vue 2 (Options API)
With scoped slots, the child component can pass data to the slot content in the parent. That way, the parent can decide how to render data coming from the child.
1. Child Component (List.vue)
<template>
<ul>
<!-- we expose each item to the slot -->
<li v-for="(item, index) in items" :key="index">
<!-- default slot gets props -->
<slot :item="item" :index="index">
<!-- fallback content if parent doesn't use scoped slot -->
{{ index }} - {{ item }}
</slot>
</li>
</ul>
</template>
<script>
export default {
name: "List",
props: {
items: {
type: Array,
required: true
}
}
}
</script>
2. Parent Component (App.vue)
<template>
<div>
<h1>Scoped Slots Example</h1>
<!-- Using scoped slot -->
<List :items="categories">
<template v-slot:default="slotProps">
<!-- slotProps.item and slotProps.index come from child -->
<strong>{{ slotProps.index + 1 }}.</strong> {{ slotProps.item.toUpperCase() }}
</template>
</List>
<!-- Without scoped slot (fallback from List.vue will be used) -->
<List :items="tags" />
</div>
</template>
<script>
import List from "./List.vue";
export default {
name: "App",
components: { List },
data() {
return {
categories: ["News", "Sports", "Entertainment"],
tags: ["vue", "laravel", "slots"]
};
}
}
</script>
Using Slots in Vue 2 (Options API)
Slots in Vue let you create reusable components where the parent controls part of the content.
1. Create a reusable component (Card.vue)
<template>
<div class="card">
<div class="card-header">
<!-- Named slot for header -->
<slot name="header">Default Header</slot>
</div>
<div class="card-body">
<!-- Default slot -->
<slot>Default content goes here...</slot>
</div>
<div class="card-footer">
<!-- Named slot for footer -->
<slot name="footer">Default Footer</slot>
</div>
</div>
</template>
<script>
export default {
name: "Card"
}
</script>
2. Use the component in a parent (App.vue or a Blade-Vue view)
<template>
<div>
<h1>Vue 2 Slots Example</h1>
<Card>
<template v-slot:header>
Custom Header: Article Info
</template>
This is the article body content coming from the parent.
<template v-slot:footer>
<button @click="sayHello">Click Me</button>
</template>
</Card>
<!-- Another card with defaults -->
<Card />
</div>
</template>
<script>
import Card from "./Card.vue";
export default {
name: "App",
components: { Card },
methods: {
sayHello() {
alert("Hello from slot footer button!");
}
}
}
</script>
Upload and Resize an Image with Laravel Vue and ImageMagick
In this example I use the Laravel and Vue frameworks to implement a SPA tool to upload images and display a list of images that have been uploaded. The application also demonstrates image resizing with the amazing PHP ImageMagick library. So there are a few prerequisits to get started however I won't cover that in this tutorial. I've added some links to get you through the prerequisits:
- Install imagemagick and the pecl imagemagick package. Configure the extension in php.
- Install and set-up Laravel to use the Vue 2 Options API with Webpack compiler.
- Create required Controllers, Blade Views, routes(web.php, api.php, routes.js) and Vue components
Assuming you have an initial setup you will need to generate an API controller to handle requests made from the Vue components. You can generate the controller with php artisan:
php artisan make:controller Api/ImageController
Next, we need to specify a directory to store these images. For this tutorial we will upload our images to /public/images. So notice in the ImageController the method public_path('images/'.$imageName) is used to specify this location.
/app/Http/Controllers/Api/ImageController.php
We need two methods to handle the functionality for this application. An index method for GET requests to retrieve a list of all images. And an upload method to handle a POST request to upload an image.
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
use Imagick;
class ImageController extends Controller
{
/**
* index
*/
public function index()
{
$fileNames = [];
$imagesPath = public_path('images/');
$files = File::files($imagesPath);
foreach ($files as $key=>$file) {
$fileNames[] = [
'file'=>$file->getFilename(),
'path'=>$file->getPathname(),
'size'=>$file->getSize(),
'ext'=>$file->getExtension()
];
}
return response()->json($fileNames);
}
/**
* upload
*/
public function upload(Request $request)
{
$request->validate([
'image' => 'required|image|mimes:jpeg,png,jpg,gif,svg|max:2048',
]);
// Create a new Imagick object
$imagick = new Imagick();
// Read the uploaded image
$imagick->readImage($request->file('image')->getPathname());
// Resample the image (change the resolution)
$imagick->resizeImage(100, 100, Imagick::FILTER_LANCZOS, 1);
// Save the image
$imageName = time() . '.' . $request->file('image')->getClientOriginalExtension();
$imagick->writeImage(public_path('images/' . $imageName));
// Clear the Imagick object
$imagick->clear();
$imagick->destroy();
$fileNames = [];
$imagesPath = public_path('images/');
$files = File::files($imagesPath);
foreach ($files as $file) {
$fileNames[] = [
'file'=>$file->getFilename(),
'path'=>$file->getPathname(),
'bytes'=>$file->getSize(),
'ext'=>$file->getExtension()
];
}
return response()->json($fileNames);
}
}
/resources/js/Image/Upload.vue
The Upload.vue component will display an image upload form and a script to interact with the form actions. Once the user selects an image, they will be able to preview the image before upload. Notice the API request made with Axios POST to /api/images. These routes are specified in /routes/api.php
<template>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
Upload Image Form
<div class="row m-1 g-1">
<div class="form-group col-md-12">
<input class="form-control" :class="[{'text-danger': 'image' in errors && errors.image.length > 0}]" type="file" @change="onFileChange" name="image" id="image" accept="image/*" >
</div>
<div v-if="'image' in errors" class="text-danger">
<span v-for="(error, index) in errors['image']" :key="'type-'+index">
{{error}}
</span>
</div>
</div>
<div v-if="preview" class="mt-3">
<img :src="preview" alt="preview" width="100" height="100" />
</div>
<div class="row m-1 g-1">
<div class="form-group col-12">
<button type="button" @click.prevent="cancelUpload" class="btn btn-sm btn-fw btn-danger float-end mx-2">
Cancel
</button>
<button class="btn btn-sm btn-fw btn-primary float-end" type="submit" @click.prevent="uploadImage">Upload Image</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
image: null,
preview: null,
user: {},
errors:{
image: []
},
};
},
methods: {
cancelUpload(){
let redirectTo = this.$route.query.redirect || { name: 'admin-images' };
this.$router.push(redirectTo);
},
async uploadImage(){
if(this.image)
{
let fd = new FormData();
fd.append('image', this.image);
try {
let resp = (await axios.post('/api/images', fd)).data.data;
let redirectTo = this.$route.query.redirect || { name: 'admin-images' };
this.$router.push(redirectTo);
} catch(error) {
this.errors = error;
}
}
else {
this.errors.image.push(`An image file is required for upload.`);
}
},
onFileChange(e) {
let file = e.target.files[0];
if (file) {
this.image = file;
this.preview = URL.createObjectURL(file);
}
}
},
created() {
console.log('Image/Upload COMPONENT CREATED.');
}
}
</script>
/resources/admin/js/Image/Index.vue
The Index.vue component will display a list of images that currently exist in the /public/images directory. Also notice the API GET request made to /api/images.
<template>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-10">
<router-link
class="btn btn-primary mb-2 float-right"
:to="{ name: 'image-upload', query: {redirect: $route.fullPath} }">
Image Upload
</router-link>
<div class="mb-4 row" v-for="(image, index) in images" :key="index">
<img
:src="`/images/${image.file}`"
class="img-thumbnail "
width="100"
/>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
images: [],
user: {}
};
},
methods: {
},
async created() {
let response = await axios.get("/api/images");
this.images = response.data.length ? response.data : [];
}
};
</script>
/resources/js/routes.js
In order to see the Image/Index.vue and Image/Upload.vue components we need to specify a route for each in routes.js. Because the path /images and /image/upload routes are handled via Vue we need to use a <router-link> inside of another Vue component order to navigate to /images and /image/upload.
Then configure the routes in /resources/js/routes.js like below:
import ImageIndex from "./Image/Index";
import ImageUpload from "./Image/Upload";
const routes = [
// add these 2 new routes
{
path: "/images",
component: ImageIndex,
name: "image-index",
},
{
path: "/image/upload",
component: ImageUpload,
name: "image-upload",
}
];
/routes/api.php
Finally we create API routes to upload and retrieve images. Notice both routes both go to /images and either Route::get or Route::post will be used to identify them properly in a request.
// GET request to ‘/api/images’
Route::get('/images', [App\Http\Controllers\Api\ImageController::class, 'index']);
// POST Request to ‘/api/images’
Route::post('/images', [App\Http\Controllers\Api\ImageController::class, 'upload']);