In one sentence

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.

Why it matters

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 big picture

The create cycle: Post / Redirect / Get

๐Ÿ“GET /posts/createcreate() shows the form
๐Ÿ“ฎPOST /postsstore() validates & saves
โ†ช๏ธ302 redirect+ flash "Post created!"
๐Ÿ‘€GET /posts/{post}show() displays it
Redirecting after a POST means refreshing the page won't resubmit the form. This pattern is called Post/Redirect/Get.
resources/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
Step by step

Build the read and create screens

We start from the PostController and Route::resource('posts', ...) from Resource Controllers, and the StorePostRequest from Forms & Validation.

  1. 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>
            
  2. index: list posts with pagination

    Fetch 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() }}
              @endsection
            

    Passing the whole $post to route('posts.show', $post) is fine: Laravel pulls out its id (or slug, if you changed the route key) automatically.

  3. show: one post via route model binding

    The {post} URL segment becomes a Post object before your method runs. A bad id gives a 404, so there's no if needed.

    
              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
            
  4. create: show the form

    The 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
            
  5. store: validate, save, redirect with a flash

    The 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 the slug rule from StorePostRequest.)

    
              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 auth middleware so it's never null. Authentication arrives in Guards & Protecting Routes.

Common mistakes

Avoid this, do that

โŒ Avoid

          $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.

โœ… Do

          return redirect()->route('posts.show', $post)
              ->with('status', 'Post created!');
        

Always redirect after a successful POST (Post/Redirect/Get).

โŒ Avoid

          <div>{!! $post->body !!}</div>
        

{!! !!} prints raw HTML, so a post containing a <script> tag runs in every reader's browser.

โœ… Do

          <div>{{ $post->body }}</div>
        

{{ }} escapes output. Use it for anything a user typed.

๐Ÿ’ก Pro tip: keep line breaks safely

{!! nl2br(e($post->body)) !!} escapes the text first with e(), then turns newlines into <br>. Safe, and paragraphs survive.

๐Ÿ‹๏ธ Try it yourself
  1. Build the layout and the three views, then visit /posts, /posts/create and a single post.
  2. Create a post and confirm the "Post created!" message appears once and disappears on refresh.
  3. Show each post's category name on the index page, and add category to the with() call.
  4. Visit /posts/999999 and confirm you get a 404.
  5. Bonus: let readers filter the index by category with ?category=3 using $request->integer('category') and when().
Recap

Key takeaways

Check yourself

Mini quiz