Laravel Vue 3 Draft Post Save - WordPress JSON API

Laravel Vue 3 Draft Post Save - WordPress JSON API

 Laravel Migration & Model

<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
    public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('content')->nullable();
            $table->string('status')->default('draft'); // draft, published
            $table->foreignId('author_id')->constrained('users')->cascadeOnDelete();
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};

//Model namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Post extends Model { use HasFactory; protected $fillable = [ 'title', 'content', 'status', 'author_id', ]; // Author relationship public function author() { return $this->belongsTo(User::class, 'author_id'); } }

Laravel Controller (API)

<?php
namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Post;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;

class PostController extends Controller
{
    // Save draft
    public function saveDraft(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'title' => 'required|string|max:255',
            'content' => 'nullable|string',
        ]);

        if ($validator->fails()) {
            return response()->json([
                'status' => 'error',
                'message' => $validator->errors(),
            ], 422);
        }

        $post = Post::updateOrCreate(
            ['id' => $request->id], // update if ID exists
            [
                'title' => $request->title,
                'content' => $request->content ?? '',
                'status' => 'draft',
                'author_id' => Auth::id() ?? 1, // Auth user or default
            ]
        );

        return response()->json([
            'status' => 'success',
            'message' => 'Post saved as draft',
            'data' => [
                'id' => $post->id,
                'title' => $post->title,
                'content' => $post->content,
                'status' => $post->status,
                'author_id' => $post->author_id,
                'created_at' => $post->created_at,
                'updated_at' => $post->updated_at,
            ],
        ]);
    }
}

API Route

<?php
use App\Http\Controllers\Api\PostController;

Route::middleware('auth:sanctum')->group(function () {
    Route::post('/posts/draft', [PostController::class, 'saveDraft']);
});

Vue.js 3 Component

<?php
<template>
  <div class="p-4">
    <input v-model="title" placeholder="Post Title" class="border p-2 w-full mb-2"/>
    <textarea v-model="content" placeholder="Post Content" class="border p-2 w-full mb-2"></textarea>
    <button @click="saveDraft" class="bg-blue-500 text-white px-4 py-2 rounded">Save Draft</button>

    <div v-if="response" class="mt-4 p-2 border">
      <pre>{{ response }}</pre>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import axios from 'axios';

const title = ref('');
const content = ref('');
const response = ref(null);

const saveDraft = async () => {
  try {
    const res = await axios.post('/api/posts/draft', {
      title: title.value,
      content: content.value,
    });
    response.value = res.data;
  } catch (err) {
    response.value = err.response?.data || err.message;
  }
};
</script>

<style scoped>
/* optional tailwind styling already used */
</style>

Features:

  1. Supports update draft if ID is provided.
  2. Returns JSON similar to WordPress.
  3. Status is fixed to "draft".
  4. Ready to integrate with Laravel Sanctum for auth.