The "C" and "R" of CRUD are four controller actions (index, show, create, store) that list posts, display one, show a form, and save what the form sends.
๐Think of a library. The catalogue is index, picking one book off the shelf is show, the donation form is create, and the librarian shelving your donated book is store.
This is the backbone of most web apps
Everything connects here
Routes, controllers, models, validation and Blade all meet in one real feature.
A reusable pattern
Build it once for posts and you can build it for categories, products or tickets in minutes.
Users get feedback
Redirects plus flash messages tell people their action worked, and stop accidental double submits.
The create cycle: Post / Redirect / Get
create() shows the formstore() validates & savesshow() displays itresources/views/ โโโ layouts/ โ โโโ app.blade.php โ shared shell + flash message โโโ posts/ โโโ index.blade.php โ list (this lesson) โโโ show.blade.php โ one post (this lesson) โโโ create.blade.php โ new form (this lesson) โโโ edit.blade.php โ next lesson
Build the read and create screens
We start from the PostController and Route::resource('posts', ...) from Resource Controllers, and the StorePostRequest from Forms & Validation.
-
A layout that shows flash messages
A flash message is a value stored in the session for exactly one request. We'll set it after saving and read it here with
session('status'), so every page can show it.<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>@yield('title', 'My Blog')</title> </head> <body> @if (session('status')) <div class="alert">{{ session('status') }}</div> @endif @yield('content') </body> </html> -
index: list posts with paginationFetch the newest posts, a page at a time.
with('user')loads each post's author in one extra query instead of one per post.public function index() { $posts = Post::with('user')->latest()->paginate(10); return view('posts.index', compact('posts')); }@extends('layouts.app') @section('content') <h1>Posts</h1> <a href="{{ route('posts.create') }}">New post</a> @forelse ($posts as $post) <article> <h2><a href="{{ route('posts.show', $post) }}">{{ $post->title }}</a></h2> <p>by {{ $post->user->name }} ยท {{ $post->created_at->diffForHumans() }}</p> </article> @empty <p>No posts yet. Write the first one!</p> @endforelse {{ $posts->links() }} @endsectionPassing the whole
$posttoroute('posts.show', $post)is fine: Laravel pulls out its id (or slug, if you changed the route key) automatically. -
show: one post via route model bindingThe
{post}URL segment becomes aPostobject before your method runs. A bad id gives a 404, so there's noifneeded.public function show(Post $post) { $post->load('user', 'category', 'comments.user'); return view('posts.show', compact('post')); }@extends('layouts.app') @section('title', $post->title) @section('content') <h1>{{ $post->title }}</h1> <p>by {{ $post->user->name }} in {{ $post->category?->name ?? 'Uncategorised' }}</p> <div>{{ $post->body }}</div> <h2>Comments ({{ $post->comments->count() }})</h2> @foreach ($post->comments as $comment) <p><strong>{{ $comment->user->name }}:</strong> {{ $comment->body }}</p> @endforeach <a href="{{ route('posts.index') }}">โ All posts</a> @endsection -
create: show the formThe form needs the categories for a dropdown. Note
@csrf,old()and@error, all from the previous lesson.public function create() { $categories = Category::orderBy('name')->get(); return view('posts.create', compact('categories')); }@extends('layouts.app') @section('content') <form method="POST" action="{{ route('posts.store') }}"> @csrf <label>Title <input name="title" value="{{ old('title') }}"></label> @error('title') <p class="error">{{ $message }}</p> @enderror <label>Category <select name="category_id"> <option value="">None</option> @foreach ($categories as $category) <option value="{{ $category->id }}" @selected(old('category_id') == $category->id)> {{ $category->name }} </option> @endforeach </select> </label> <label>Body <textarea name="body">{{ old('body') }}</textarea></label> @error('body') <p class="error">{{ $message }}</p> @enderror <button type="submit">Publish</button> </form> @endsection -
store: validate, save, redirect with a flashThe Form Request validates first. We build the slug from the title, create the post through the logged-in user, then redirect with
->with('status', ...), which flashes the message for the next page. (Since we generate the slug here, remove theslugrule fromStorePostRequest.)use App\Http\Requests\StorePostRequest; use Illuminate\Support\Str; public function store(StorePostRequest $request) { $post = $request->user()->posts()->create([ ...$request->validated(), 'slug' => Str::slug($request->title) . '-' . Str::lower(Str::random(5)), ]); return redirect() ->route('posts.show', $post) ->with('status', 'Post created!'); }โน๏ธ Who is$request->user()?The logged-in user. Protect the create and store routes with the
authmiddleware so it's nevernull. Authentication arrives in Guards & Protecting Routes.
Avoid this, do that
$post = Post::create(...);
return view('posts.show', compact('post'));
Returning a view from a POST means pressing refresh resubmits the form and creates a duplicate post.
return redirect()->route('posts.show', $post)
->with('status', 'Post created!');
Always redirect after a successful POST (Post/Redirect/Get).
<div>{!! $post->body !!}</div>
{!! !!} prints raw HTML, so a post containing a <script> tag runs in every reader's browser.
<div>{{ $post->body }}</div>
{{ }} escapes output. Use it for anything a user typed.
{!! nl2br(e($post->body)) !!} escapes the text first with e(), then turns newlines into <br>. Safe, and paragraphs survive.
- Build the layout and the three views, then visit
/posts,/posts/createand a single post. - Create a post and confirm the "Post created!" message appears once and disappears on refresh.
- Show each post's category name on the index page, and add
categoryto thewith()call. - Visit
/posts/999999and confirm you get a 404. - Bonus: let readers filter the index by category with
?category=3using$request->integer('category')andwhen().
Key takeaways
indexlists (paginate + eager load),showdisplays one,createshows a form,storesaves.- Route model binding turns
{post}into aPost, with a free 404. - Every form that changes data includes
@csrf. - Redirect after POST, and flash feedback with
->with('status', '...'). - Read flash messages in the layout with
session('status'). - Print user content with
{{ }}, never raw{!! !!}.