--- title: Blog Tutorial (short) order: 3 hidden: true --- # Blog Tutorial We're going to be short on words and quick on code in this quickstart. If you're looking to see what Remix is all about in 15 minutes, this is it. Work through this tutorial with Kent in this free Egghead.io course This tutorial uses TypeScript. Remix can definitely be used without TypeScript. We feel most productive when writing TypeScript, but if you'd prefer to skip the TypeScript syntax, feel free to write your code in JavaScript. πŸ’Ώ Hey I'm Derrick the Remix Compact Disc πŸ‘‹ Whenever you're supposed to _do_ something you'll see me ## Prerequisites Click this button to create a [Gitpod][gitpod] workspace with the project set up and ready to run in VS Code or JetBrains either directly in the browser or on the desktop. [![Gitpod Ready-to-Code][gitpod-ready-to-code]][gitpod-ready-to-code-image] If you want to follow this tutorial locally on your own computer, it is important for you to have these things installed: - [Node.js][node-js] version (>=18.0.0) - [npm][npm] 7 or greater - A code editor ([VSCode][vs-code] is a nice one) ## Creating the project Make sure you are running at least Node v18 or greater πŸ’Ώ Initialize a new Remix project. We'll call ours "blog-tutorial" but you can call it something else if you'd like. ```shellscript nonumber npx create-remix@latest --template remix-run/indie-stack blog-tutorial ``` ``` Install dependencies with npm? Yes ``` You can read more about the stacks available in [the stacks docs][the-stacks-docs]. We're using [the Indie stack][the-indie-stack], which is a full application ready to deploy to [fly.io][fly-io]. This includes development tools as well as production-ready authentication and persistence. Don't worry if you're unfamiliar with the tools used, we'll walk you through things as we go. Note, you can definitely start with "Just the basics" instead by running `npx create-remix@latest` without the `--template` flag. The generated project is much more minimal that way. However, some bits of the tutorial will be different for you and you'll have to configure things for deployment manually. πŸ’Ώ Now, open the project that was generated in your preferred editor and check the instructions in the `README.md` file. Feel free to read over this. We'll get to the deployment bit later in the tutorial. πŸ’Ώ Let's start the dev server: ```shellscript nonumber npm run dev ``` πŸ’Ώ Open up [http://localhost:3000][http-localhost-3000], the app should be running. If you want, take a minute and poke around the UI a bit. Feel free to create an account and create/delete some notes to get an idea of what's available in the UI out of the box. ## Your First Route We're going to make a new route to render at the "/posts" URL. Before we do that, let's link to it. πŸ’Ώ Add a link to posts in `app/routes/_index.tsx` Go ahead and copy/paste this: ```tsx filename=app/routes/_index.tsx
Blog Posts
``` You can put it anywhere you like. I stuck it right above the icons of all the technologies used in the stack: ![Screenshot of the app showing the blog post link][screenshot-of-the-app-showing-the-blog-post-link] You may have noticed we're using Tailwind CSS classes. The Remix Indie stack has [Tailwind CSS][tailwind] support pre-configured. If you'd prefer to not use Tailwind CSS, you're welcome to remove it and use something else. Learn more about your styling options with Remix in [the styling guide][the-styling-guide]. Back in the browser go ahead and click the link. You should see a 404 page since we've not created this route yet. Let's create the route now: πŸ’Ώ Create a new file at `app/routes/posts._index.tsx` ```shellscript nonumber touch app/routes/posts._index.tsx ``` Any time you see terminal commands to create files or folders, you can of course do that however you'd like, but using `touch` is just a way for us to make it clear which files you should be creating. We could have named it just `posts.tsx` but we'll have another route soon, and it'll be nice to put them by each other. An index route will render at the parent's path (just like `index.html` on a web server). Now if you navigate to the `/posts` route, you'll get an error indicating there's no way to handle the request. That's because we haven't done anything in that route yet! Let's add a component and export it as the default: πŸ’Ώ Make the posts component ```tsx filename=app/routes/posts._index.tsx export default function Posts() { return (

Posts

); } ``` You might need to refresh the browser to see our new, bare-bones posts route. ## Loading Data Data loading is built into Remix. If your web dev background is primarily in the last few years, you're probably used to creating two things here: an API route to provide data and a frontend component that consumes it. In Remix your frontend component is also its own API route, and it already knows how to talk to itself on the server from the browser. That is, you don't have to fetch it. If your background is a bit farther back than that with MVC web frameworks like Rails, then you can think of your Remix routes as backend views using React for templating, but then they know how to seamlessly hydrate in the browser to add some flair instead of writing detached jQuery code to dress up the user interactions. It's progressive enhancement realized in its fullest. Additionally, your routes are their own controller. So let's get to it and provide some data to our component. πŸ’Ώ Make the posts route `loader` ```tsx filename=app/routes/posts._index.tsx lines=[1-2,4-17,20-21] import { json } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; export const loader = async () => { return json({ posts: [ { slug: "my-first-post", title: "My First Post", }, { slug: "90s-mixtape", title: "A Mixtape I Made Just For You", }, ], }); }; export default function Posts() { const { posts } = useLoaderData(); return (

Posts

); } ``` `loader` functions are the backend "API" for their component, and it's already wired up for you through `useLoaderData`. It's a little wild how blurry the line is between the client and the server in a Remix route. If you have your server and browser consoles both open, you'll note that they both logged our post data. That's because Remix rendered on the server to send a full HTML document like a traditional web framework, but it also hydrated in the client and logged there too. Whatever you return from your loader will be exposed to the client, even if the component doesn't render it. Treat your loaders with the same care as public API endpoints. πŸ’Ώ Render links to our posts ```tsx filename=app/routes/posts._index.tsx lines=[2,10-21] nocopy import { json } from "@remix-run/node"; import { Link, useLoaderData } from "@remix-run/react"; // ... export default function Posts() { const { posts } = useLoaderData(); return (

Posts

    {posts.map((post) => (
  • {post.title}
  • ))}
); } ``` Hey, that's pretty cool. We get a pretty solid degree of type safety even over a network request because it's all defined in the same file. Unless the network blows up while Remix fetches the data, you've got type safety in this component and its API (remember, the component is already its own API route). ## A little refactoring A solid practice is to create a module that deals with a particular concern. In our case it's going to be reading and writing posts. Let's set that up now and add a `getPosts` export to our module. πŸ’Ώ Create `app/models/post.server.ts` ```shellscript nonumber touch app/models/post.server.ts ``` We're mostly going to copy/paste stuff from our route: ```tsx filename=app/models/post.server.ts type Post = { slug: string; title: string; }; export async function getPosts(): Promise> { return [ { slug: "my-first-post", title: "My First Post", }, { slug: "90s-mixtape", title: "A Mixtape I Made Just For You", }, ]; } ``` Note that we're making the `getPosts` function `async` because even though it's not currently doing anything async it will soon! πŸ’Ώ Update the posts route to use our new posts module: ```tsx filename=app/routes/posts._index.tsx lines=[4,6-8] nocopy import { json } from "@remix-run/node"; import { Link, useLoaderData } from "@remix-run/react"; import { getPosts } from "~/models/post.server"; export const loader = async () => { return json({ posts: await getPosts() }); }; // ... ``` ## Pulling from a data source With the Indie Stack, we've got a SQLite database already set up and configured for us, so let's update our Database Schema to handle SQLite. We're using [Prisma][prisma] to interact with the database, so we'll update that schema and Prisma will take care of updating our database to match the schema for us (as well as generating and running the necessary SQL commands for the migration). You do not have to use Prisma when using Remix. Remix works great with whatever existing database or data persistence services you're currently using. If you've never used Prisma before, don't worry, we'll walk you through it. πŸ’Ώ First, we need to update our Prisma schema: ```prisma filename=prisma/schema.prisma nocopy // Stick this at the bottom of that file: model Post { slug String @id title String markdown String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } ``` πŸ’Ώ Let's generate a migration file for our schema changes, which will be required if you deploy your application rather than just running in dev mode locally. This will also update our local database and TypeScript definitions to match the schema change. We'll name the migration "create post model". ```shellscript nonumber npx prisma migrate dev --name "create post model" ``` πŸ’Ώ Let's seed our database with a couple posts. Open `prisma/seed.ts` and add this to the end of the seed functionality (right before the `console.log`): ```ts filename=prisma/seed.ts const posts = [ { slug: "my-first-post", title: "My First Post", markdown: ` # This is my first post Isn't it great? `.trim(), }, { slug: "90s-mixtape", title: "A Mixtape I Made Just For You", markdown: ` # 90s Mixtape - I wish (Skee-Lo) - This Is How We Do It (Montell Jordan) - Everlong (Foo Fighters) - Ms. Jackson (Outkast) - Interstate Love Song (Stone Temple Pilots) - Killing Me Softly With His Song (Fugees, Ms. Lauryn Hill) - Just a Friend (Biz Markie) - The Man Who Sold The World (Nirvana) - Semi-Charmed Life (Third Eye Blind) - ...Baby One More Time (Britney Spears) - Better Man (Pearl Jam) - It's All Coming Back to Me Now (CΓ©line Dion) - This Kiss (Faith Hill) - Fly Away (Lenny Kravits) - Scar Tissue (Red Hot Chili Peppers) - Santa Monica (Everclear) - C'mon N' Ride it (Quad City DJ's) `.trim(), }, ]; for (const post of posts) { await prisma.post.upsert({ where: { slug: post.slug }, update: post, create: post, }); } ``` Note that we're using `upsert` so you can run the seed script over and over without adding multiple versions of the same post every time. Great, let's get those posts into the database with the seed script: ``` npx prisma db seed ``` πŸ’Ώ Now update the `app/models/post.server.ts` file to read from the SQLite database: ```ts filename=app/models/post.server.ts import { prisma } from "~/db.server"; export async function getPosts() { return prisma.post.findMany(); } ``` Notice we're able to remove the return type, but everything is still fully typed. The TypeScript feature of Prisma is one of its greatest strengths. Less manual typing, but still type safe! The `~/db.server` import is importing the file at `app/db.server.ts`. The `~` is a fancy alias to the `app` directory, so you don't have to worry about how many `../../`s to include in your import as you move files around. You should be able to go to `http://localhost:3000/posts` and the posts should still be there, but now they're coming from SQLite! ## Dynamic Route Params Now let's make a route to actually view the post. We want these URLs to work: ``` /posts/my-first-post /posts/90s-mixtape ``` Instead of creating a route for every single one of our posts, we can use a "dynamic segment" in the url. Remix will parse and pass to us, so we can look up the post dynamically. πŸ’Ώ Create a dynamic route at `app/routes/posts.$slug.tsx` ```shellscript nonumber touch app/routes/posts.\$slug.tsx ``` ```tsx filename=app/routes/posts.$slug.tsx export default function PostSlug() { return (

Some Post

); } ``` You can click one of your posts and should see the new page. πŸ’Ώ Add a loader to access the params ```tsx filename=app/routes/posts.$slug.tsx lines=[1-3,5-9,12,16] import type { LoaderFunctionArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; export const loader = async ({ params, }: LoaderFunctionArgs) => { return json({ slug: params.slug }); }; export default function PostSlug() { const { slug } = useLoaderData(); return (

Some Post: {slug}

); } ``` The part of the filename attached to the `$` becomes a named key on the `params` object that comes into your loader. This is how we'll look up our blog post. Now, let's actually get the post contents from the database by its slug. πŸ’Ώ Add a `getPost` function to our post module ```tsx filename=app/models/post.server.ts lines=[7-9] import { prisma } from "~/db.server"; export async function getPosts() { return prisma.post.findMany(); } export async function getPost(slug: string) { return prisma.post.findUnique({ where: { slug } }); } ``` πŸ’Ώ Use the new `getPost` function in the route ```tsx filename=app/routes/posts.$slug.tsx lines=[5,10-11,15,19] import type { LoaderFunctionArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; import { getPost } from "~/models/post.server"; export const loader = async ({ params, }: LoaderFunctionArgs) => { const post = await getPost(params.slug); return json({ post }); }; export default function PostSlug() { const { post } = useLoaderData(); return (

{post.title}

); } ``` Check that out! We're now pulling our posts from a data source instead of including it all in the browser as JavaScript. Let's make TypeScript happy with our code: ```tsx filename=app/routes/posts.$slug.tsx lines=[4,11,14] import type { LoaderFunctionArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; import invariant from "tiny-invariant"; import { getPost } from "~/models/post.server"; export const loader = async ({ params, }: LoaderFunctionArgs) => { invariant(params.slug, "params.slug is required"); const post = await getPost(params.slug); invariant(post, `Post not found: ${params.slug}`); return json({ post }); }; export default function PostSlug() { const { post } = useLoaderData(); return (

{post.title}

); } ``` Quick note on that `invariant` for the params. Because `params` comes from the URL, we can't be totally sure that `params.slug` will be defined--maybe you change the name of the file to `posts.$postId.ts`! It's a good practice to validate that stuff with `invariant`, and it makes TypeScript happy too. We also have an invariant for the post. We'll handle the `404` case better later. Keep going! Now let's get that markdown parsed and rendered to HTML to the page. There are a lot of Markdown parsers, we'll use `marked` for this tutorial because it's really easy to get working. πŸ’Ώ Parse the markdown into HTML ```shellscript nonumber npm add marked@^4.3.0 # additionally, if using typescript npm add @types/marked@^4.3.1 -D ``` Now that `marked` has been installed, we will need to restart our server. So stop the dev server and start it back up again with `npm run dev`. ```tsx filename=app/routes/posts.$slug.tsx lines=[4,17-18,22,28] import type { LoaderFunctionArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; import { marked } from "marked"; import invariant from "tiny-invariant"; import { getPost } from "~/models/post.server"; export const loader = async ({ params, }: LoaderFunctionArgs) => { invariant(params.slug, "params.slug is required"); const post = await getPost(params.slug); invariant(post, `Post not found: ${params.slug}`); const html = marked(post.markdown); return json({ html, post }); }; export default function PostSlug() { const { html, post } = useLoaderData(); return (

{post.title}

); } ``` Holy smokes, you did it. You have a blog. Check it out! Next, we're going to make it easier to create new blog posts πŸ“ ## Nested Routing Right now, our blog posts just come from seeding the database. Not a real solution, so we need a way to create a new blog post in the database. We're going to be using actions for that. Let's make a new "admin" section of the app. πŸ’Ώ First, let's add a link to the admin section on the posts index route: ```tsx filename=app/routes/posts._index.tsx // ... Admin // ... ``` Put that anywhere in the component. I stuck it right under the `

`. Did you notice that the `to` prop is just "admin" and it linked to `/posts/admin`? With Remix, you get relative links. πŸ’Ώ Create an admin route at `app/routes/posts.admin.tsx`: ```shellscript nonumber touch app/routes/posts.admin.tsx ``` ```tsx filename=app/routes/posts.admin.tsx import { json } from "@remix-run/node"; import { Link, useLoaderData } from "@remix-run/react"; import { getPosts } from "~/models/post.server"; export const loader = async () => { return json({ posts: await getPosts() }); }; export default function PostAdmin() { const { posts } = useLoaderData(); return (

Blog Admin

...
); } ``` You should recognize several of the things we're doing in there from what we've done so far. With that, you should have a decent looking page with the posts on the left and a placeholder on the right. Now, if you click on the Admin link, it'll take you to [http://localhost:3000/posts/admin][http-localhost-3000-posts-admin]. ### Index Routes Let's fill in that placeholder with an index route for admin. Hang with us, we're introducing "nested routes" here where your route file nesting becomes UI component nesting. πŸ’Ώ Create an index route for `posts.admin.tsx`'s child routes ```shellscript nonumber touch app/routes/posts.admin._index.tsx ``` ```tsx filename=app/routes/posts.admin._index.tsx import { Link } from "@remix-run/react"; export default function AdminIndex() { return (

Create a New Post

); } ``` If you refresh you're not going to see it yet. Every route that starts with `app/routes/posts.admin.` can now render _inside_ of `app/routes/posts.admin.tsx` when their URL matches. You get to control which part of the `posts.admin.tsx` layout the child routes render. πŸ’Ώ Add an outlet to the admin page ```tsx filename=app/routes/posts.admin.tsx lines=[4,37] import { json } from "@remix-run/node"; import { Link, Outlet, useLoaderData, } from "@remix-run/react"; import { getPosts } from "~/models/post.server"; export const loader = async () => { return json({ posts: await getPosts() }); }; export default function PostAdmin() { const { posts } = useLoaderData(); return (

Blog Admin

); } ``` Hang with us for a minute, index routes can be confusing at first. Just know that when the URL matches the parent route's path, the index will render inside the `Outlet`. Maybe this will help, let's add the `/posts/admin/new` route and see what happens when we click the link. πŸ’Ώ Create the `app/routes/posts.admin.new.tsx` file ```shellscript nonumber touch app/routes/posts.admin.new.tsx ``` ```tsx filename=app/routes/posts.admin.new.tsx export default function NewPost() { return

New Post

; } ``` Now click the link from the index route and watch the `` automatically swap out the index route for the "new" route! ## Actions We're going to get serious now. Let's build a form to create a new post in our new "new" route. πŸ’Ώ Add a form to the new route ```tsx filename=app/routes/posts.admin.new.tsx import { Form } from "@remix-run/react"; const inputClassName = "w-full rounded border border-gray-500 px-2 py-1 text-lg"; export default function NewPost() { return (