Laravel API development has become essential with the growing popularity of mobile applications and JavaScript frameworks. Implementing a RESTful APIs is now the optimal solution for creating a unified interface between your data and client applications.
PHP continues to be one of the most widely used web programming languages today due to its simplicity in maintenance and rapid development of feature-rich web applications. Dynamic, interactive, secure, and efficient websites require robust tools for creating and consuming APIs.
In this comprehensive tutorial, you will learn how to build a modern RESTful API using Laravel. Let’s explore building a PHP RESTful API with Laravel.
Step 1: Prerequisites
Let’s examine the technologies we’ll use to build our API:
- PHP 8.2 or higher
- Laravel 12 Framework
- MySQL Database
- Composer (Dependency Manager)
- Basic understanding of PHP and HTTP methods
ServerAvatar takes care of the entire setup process for you, so there’s no need to install or configure these technologies manually. With ServerAvatar, you can easily deploy a server that’s ready with basic setup.

Step 2: Understanding Application
We will build a complete CRUD API. CRUD stands for Create, Read, Update, and Delete. Our API will include the following endpoints:
Method | URI | Name | Description |
---|---|---|---|
GET | api/posts | Index | Retrieve all posts |
GET | api/posts/{id} | Show | Get details of a specific post by ID |
POST | api/posts | Store | Create a new post |
PUT | api/posts/{id} | Update | Update a specific post by ID |
DELETE | api/posts/{id} | Destroy | Delete a specific post by ID |
Step 3: Setup New Laravel App
To begin, create a new Laravel application. Execute the following command in your terminal:
composer create-project laravel/laravel blog-api
Alternatively, if you have installed the Laravel Installer as a global composer dependency:
laravel new blog-api
Next, start the Laravel development server if it’s not already running:
Step 4: Create MySQL Database
Create a new database for your application. Login to MySQL and execute the following command:
mysql -u<username> -p<password>
To create a new database, run the following command:
CREATE DATABASE 'laravel_api';
Step 5: Create Model and Migration
We can create a Model along with migration by running the following command:
php artisan make:model Post -m
The -m argument will create a Migration in a single command.
A new file named “Post.php” will be created in the app/Models directory.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
protected $table = 'posts';
protected $fillable = [
'name',
'image',
'description'
];
}
A migration file will be created in the database/migrations directory to generate the table in our database. Modify the migration file to create columns for name, description, and image – all fields that accept string values.
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePostsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('image');
$table->text('description');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('posts');
}
}
Open the .env file and update the credentials to access your MySQL database:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_api
DB_USERNAME=your_username
DB_PASSWORD=your_password
Next, run your migration using the following command:
php artisan migrate
Step 6: Create Controller and Request
Create a resource Controller by running the following command:
php artisan make:controller PostController -r
Resource controllers make it effortless to build RESTful controllers around resources.
Next, create a Request file by running the following command:
php artisan make:request PostStoreRequest
As you may already know, there are multiple ways to validate requests in Laravel. Handling request validation is a crucial part of any application. Laravel provides excellent features for handling this efficiently.
HTTP Status Codes
We’ve incorporated the response()->json() method in our endpoints. This allows us to explicitly return JSON data and send HTTP status codes that the client can interpret. The most common codes you’ll be returning are:
- 200: OK. The standard success code and default option.
- 201: Created. Object created successfully. Useful for store actions.
- 204: No Content. When the action was executed successfully, but there is no content to return.
- 206: Partial Content. Useful when returning a paginated list of resources.
- 400: Bad Request. The standard option for requests that fail validation.
- 401: Unauthorized. The user needs to be authenticated.
- 403: Forbidden. The user is authenticated but lacks permissions to perform an action.
- 404: Not Found. Laravel will return this automatically when the resource is not found.
- 500: Internal Server Error. Ideally, you won’t be explicitly returning this, but if something unexpected occurs, this is what your user will receive.
- 503: Service Unavailable. Self-explanatory, but another code that won’t be returned explicitly by the application.
Step 7: Setup CRUD (Create, Read, Update and Delete)
1. Setup Routes
Note: All API requests will need the header Accept: application/json.
Add the routes to the API routes file to access all the functions we created.
Open “routes/api.php” and update it with the following code:
Route::get('posts', "PostController@index"); // List Posts
Route::post('posts', "PostController@store"); // Create Post
Route::get('posts/{id}', "PostController@show"); // Detail of Post
Route::put('posts/{id}', "PostController@update"); // Update Post
Route::delete('posts/{id}', "PostController@destroy"); // Delete Post
Or you can add a resource route like this:
Route::resource('posts', 'PostController');
Now, open “app/Http/Controllers/PostController.php” and update it with the following code:
2. Read All Posts
To get the list of all posts, update the following code:
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
// All Posts
$posts = Post::all();
// Return Json Response
return response()->json([
'posts' => $posts
], 200);
}
Get the details of a post by ID. Update the following code:
/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function show($id)
{
// Post Detail
$post = Post::find($id);
if (!$post) {
return response()->json([
'message' => 'Post Not Found.'
], 404);
}
// Return Json Response
return response()->json([
'post' => $post
], 200);
}
3. Create Post
Now, open “app/Http/Requests/PostStoreRequest.php” and update it with the following code:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class PostStoreRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
if (request()->isMethod('post')) {
return [
'name' => 'required|string|max:258',
'image' => 'required|image|mimes:jpeg,png,jpg,gif,svg|max:2048',
'description' => 'required|string'
];
} else {
return [
'name' => 'required|string|max:258',
'image' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048',
'description' => 'required|string'
];
}
}
/**
* Custom message for validation
*
* @return array
*/
public function messages()
{
if (request()->isMethod('post')) {
return [
'name.required' => 'Name is required!',
'image.required' => 'Image is required!',
'description.required' => 'Description is required!'
];
} else {
return [
'name.required' => 'Name is required!',
'description.required' => 'Description is required!'
];
}
}
}
Now, open “app/Http/Controllers/PostController.php” and update it with the following code:
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(PostStoreRequest $request)
{
try {
$imageName = Str::random(32) . "." . $request->image->getClientOriginalExtension();
// Create Post
Post::create([
'name' => $request->name,
'image' => $imageName,
'description' => $request->description
]);
// Save Image in Storage folder
Storage::disk('public')->put($imageName, file_get_contents($request->image));
// Return Json Response
return response()->json([
'message' => "Post successfully created."
], 200);
} catch (\Exception $e) {
// Return Json Response
return response()->json([
'message' => "Something went really wrong!"
], 500);
}
}
4. Update Post
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(PostStoreRequest $request, $id)
{
try {
// Find Post
$post = Post::find($id);
if (!$post) {
return response()->json([
'message' => 'Post Not Found.'
], 404);
}
$post->name = $request->name;
$post->description = $request->description;
if ($request->image) {
// Public storage
$storage = Storage::disk('public');
// Old image delete
if ($storage->exists($post->image))
$storage->delete($post->image);
// Image name
$imageName = Str::random(32) . "." . $request->image->getClientOriginalExtension();
$post->image = $imageName;
// Image save in public folder
$storage->put($imageName, file_get_contents($request->image));
}
// Update Post
$post->save();
// Return Json Response
return response()->json([
'message' => "Post successfully updated."
], 200);
} catch (\Exception $e) {
// Return Json Response
return response()->json([
'message' => "Something went really wrong!"
], 500);
}
}
5. Delete Post
Delete a post by ID. Update the following code:
/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy($id)
{
// Post Detail
$post = Post::find($id);
if (!$post) {
return response()->json([
'message' => 'Post Not Found.'
], 404);
}
// Public storage
$storage = Storage::disk('public');
// Image delete
if ($storage->exists($post->image))
$storage->delete($post->image);
// Delete Post
$post->delete();
// Return Json Response
return response()->json([
'message' => "Post successfully deleted."
], 200);
}
You may or may not be aware that there is an artisan command to create a symbolic link from the storage folder to the public folder.
This command allows us to access the files. By default, Laravel stores files in the storage directory while keeping the public directory clean for public files only.
This command helps us generate symbolic links:
php artisan storage:link
Don’t forget to add the necessary imports at the top of your PostController:
use App\Models\Post;
use App\Http\Requests\PostStoreRequest;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
No More Manual Setup – ServerAvatar Does It All for You!
When you’re using ServerAvatar, there’s no need to install MySQL manually, it’s all handled for you. Plus, you can streamline your workflow even further using our Git-based deployment system.
ServerAvatar is your all-in-one, easy-to-use server management platform that takes the complexity out of managing servers and applications. With a clean and intuitive dashboard, you can take care of everything, from provisioning servers and deploying applications to setting up security, backups, and software updates, without touching the terminal.
Whether you’re managing one app or dozens, ServerAvatar makes it simple to create databases, manage multiple applications, and handle several servers, all in one place, with zero hassle.
Frequently Asked Questions (FAQ)
Q1: How do I test my REST API endpoints?
You can test your API using several methods:
Postman: Popular GUI tool for API testing
cURL: Command line tool for making HTTP requests
Example cURL command:
curl -X GET http://localhost:8000/api/posts \ -H “Accept: application/json”
Q2: Why am I getting “Accept: application/json” header requirement?
Laravel requires this header to return JSON responses instead of HTML. Without this header, Laravel might return HTML error pages instead of JSON responses, which is not suitable for API clients.
Q3: How do I handle file uploads larger than 2MB?
You can modify the validation rules and PHP configuration:
Update validation in PostStoreRequest.php:
‘image’ => ‘required|image|mimes:jpeg,png,jpg,gif,svg|max:10240’, // 10MB
Update PHP settings in php.ini:
upload_max_filesize = 10M post_max_size = 10M
Q4: What if I get “Storage link already exists” error?
This happens when the symbolic link already exists. You can either:
Remove the existing link: rm public/storage
Or ignore the error if the link is working properly
Q5: How do I add authentication to my API?
Laravel offers several authentication options:
Sanctum: For SPA and mobile applications
Passport: For OAuth2 implementation
Example with Sanctum:
composer require laravel/sanctum php artisan vendor:publish –provider=”Laravel\Sanctum\SanctumServiceProvider”
Q6: What about CORS issues when calling API from frontend?
Laravel includes CORS middleware. Configure it in config/cors.php:
‘allowed_origins’ => [‘http://localhost:3000’], // Your frontend URL ‘allowed_methods’ => [‘*’], ‘allowed_headers’ => [‘*’],
Conclusion
You have successfully built a complete REST API using Laravel 12 with full CRUD functionality. This API includes proper validation, error handling, image upload capabilities, and follows REST conventions.
Your API is now ready to handle:
- Creating new posts with image uploads
- Retrieving all posts or specific posts by ID
- Updating existing posts with optional image replacement
- Deleting posts along with their associated images
Remember to test your API endpoints using tools like Postman or any other API testing tool with the proper headers and request formats.