Happy Employees == Happy ClientsCAREERS AT DEPT®
DEPT® Engineering BlogPlatforms

Rapid Enterprise Development with RedwoodJS

Get an overview of the RedwoodJS framework by following this quick tutorial and building a sample application.

Redwood quietly entered the framework world in March 2020, with an enterprise first approach as “the framework for Startups” (Next.js, Remix, etc). If you haven’t heard of Redwood it was founded and created by Tom Preston-Warner, co-founder of Github, creator of TOML language, and many other ventures.

Redwood describes itself as,an opinionated, full-stack, JavaScript/TypeScript web application framework designed to keep you moving fast as your app grows from side project to startup.”

What makes RedwoodJs unique as a framework is that it doesn’t exactly reinvent the wheel, but instead uses industry standard tools that we would already use in an opinionated full-stack framework. It uses creative and smart integrations with boilerplate code that feels like the monolith we never knew we needed. The backend stack runs on Prisma, Graphql, and Node. This is independent from the frontend, but easily integrates with it using "cells”.

A cell in Redwood is a collection of frontend code used to query to API layer that allows you to quickly add boilerplate code you need to get an application to MVP quickly and easily. Just as the backend uses industry standard tools, the frontend follows that same pattern. When scaffolding your component, you also get a full component built out for you including a storybook story already ready to view before you begin development, Jest tests setup and working, and a functional base component.

Let's build a quick to-do app connected to a Postgres database on Railway. We will explore installing and setting up a Redwood app, setup a schema, scaffold our component, set up some routing, and quickly add styles.

Setting up Redwood

Setting up Redwood is quite easy. First we will use the redwood-app package to install our project:

yarn create redwood-app dept-todos --typescript

This will create our project, initialize a git repo, and perform our init commit. Now we can swap to the dept-todos folder created during setup:

cd dept-todos

Next, we will want to set up Tailwind on our project. With Redwood that is also a very simple CLI command:

yarn rw setup ui tailwindcss

Side note before continuing...
If you didn’t want to use tailwind in a project, it is just as easy to setup a sass configuration. Instead of running the tailwind setup above, you could simply run the following command to add a sass setup:

yarn workspace web add -D sass sass-loader

Adding a schema

Redwood uses Prisma as the ORM for the API layer. If you are not familiar with Prisma, they have great documentation and use cases on their website.

After creating a postgres database on Railway and adding the postgres address to our .env file at the root of our directory, we are now ready to set our schema.

We will keep this schema very simple with one model for our todos, located at api/db/schema.prisma.

Replace the contents with our new todo schema:

datasource db {
    provider = "postgresql"	
    url = env("DATABASE_URL")
generator client {
    provider = "prisma-client-js"
    binaryTargets = "native"

model Todo {
    id        Int      @id @default(autoincrement())
    body      String   @db.VarChar(255)	
    completed Boolean  @default(false)	
    createdAt DateTime @default(now())	
    updatedAt DateTime @updatedAt

As you can see we have an id, body, completed status, and a timestamp - a very simple setup for our todos. We now need to migrate and deploy our schema changes to our database. This can be done easily with Prisma.

yarn redwood prisma migrate dev

Prisma will run its migration CLI tool and prompt you to name the migration for your schema changes. We just named this migration todos model.

We have now saved a migration file to our repository and migrated the changes to our database on Prisma.

Now that the database changes are active, let's scaffold our Todos. Scaffolding is easy and should be familiar to those who have worked with Ruby/Rails, we can run the following command to get our todo’s component ready.

yarn rw generate scaffold todo

That created a folder in our components on the frontend, set up the associated routing in our router file, and set the the API files needed for basic CRUD functionality.

As you can see, Redwood set up our todo’s component on the frontend and a CRUD file for the API.

Our Scaffolding created components, pages, services, a todo layout, and more for us with one simple command. This can be a very time consuming step during the initial setup of a traditional application and with Redwood we are able to go from install to running in less than 5 minutes.

Start up your development server and see the todo app in action!

yarn rw dev

The Vite instance and your API layer will start and you should be able to see your site now live at http://localhost:8910/.

While our app is already functional, it’s not really great to use out of the box. In the next section we will style and refactor some of the boilerplate files to improve the user experience and deploy our application!

Time to style

Now that we have our core API setup for an app, let’s create a homepage to view our app. We can use the CLI for this as well.

yarn redwood generate page home /

This adds a homepage route to our Router. We will use this page during our refactor to make our app more usable. It can be found at /web/src/Routes.tsx.

In the Router, you will see our todo model was added under the todos route during the scaffolding process:

import { Set, Router, Route } from '@redwoodjs/router'

import ScaffoldLayout from 'src/layouts/ScaffoldLayout'

const Routes = () => {
  return (
      <Set wrap={ScaffoldLayout} title="Todos" titleTo="todos" buttonLabel="New Todo" buttonTo="newTodo">
        <Route path="/todos/new" page={TodoNewTodoPage} name="newTodo" />
        <Route path="/todos/{id:Int}/edit" page={TodoEditTodoPage} name="editTodo" />
        <Route path="/todos/{id:Int}" page={TodoTodoPage} name="todo" />
        <Route path="/todos" page={TodoTodosPage} name="todos" />
      <Route notfound page={NotFoundPage} />

export default Routes

Let’s modify the router so our todos will render on the root path:

import { Set, Router, Route } from '@redwoodjs/router'

import ScaffoldLayout from 'src/layouts/ScaffoldLayout'

const Routes = () => {
  return (
      <Set wrap={ScaffoldLayout} title="Todos" titleTo="todos" buttonLabel="New Todo" buttonTo="newTodo">
        <Route path="/new" page={TodoNewTodoPage} name="newTodo" />
        <Route path="/{id:Int}/edit" page={TodoEditTodoPage} name="editTodo" />
        <Route path="/{id:Int}" page={TodoTodoPage} name="todo" />
        <Route path="/" page={TodoTodosPage} name="todos" />
      <Route notfound page={NotFoundPage} />

export default Routes

Excellent! Now you can see our vanilla scaffold functional on our root path.

View of the homepage root of our application

When we generated the scaffold earlier, we also created a todos folder that contains both our components and our cells, the frontend code that will host our graphql calls and hydrate our components.

Structure of the scaffolded Todo model

First we are going to install Lucide icons to update the generic links. We do this by first navigating into our web directory where react is present:

cd web
yarn add lucide-react

Then we can replace the contents of Todos component at  web/src/components/Todo/Todos/Todos.tsx with the following snippet:

import { FileEdit, XCircle } from 'lucide-react'
import type {
} from 'types/graphql'

import { Form, CheckboxField } from '@redwoodjs/forms'
import { Link, navigate, routes } from '@redwoodjs/router'
import { useMutation } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'

import { QUERY } from 'src/components/Todo/TodosCell'
import { truncate } from 'src/lib/formatters'

  mutation DeleteTodoMutation($id: Int!) {
    deleteTodo(id: $id) {

  mutation UpdateTodoMutation($id: Int!, $input: UpdateTodoInput!) {
    updateTodo(id: $id, input: $input) {

const TodosList = ({ todos }: FindTodos) => {
  const [deleteTodo] = useMutation(DELETE_TODO_MUTATION, {
    onCompleted: () => {
      toast.success('Todo deleted')
    onError: (error) => {
    refetchQueries: [{ query: QUERY }],
    awaitRefetchQueries: true,

  const onDeleteClick = (id: DeleteTodoMutationVariables['id']) => {
    if (confirm('Are you sure you want to delete todo ' + id + '?')) {
      deleteTodo({ variables: { id } })

  const [updateTodo] = useMutation(UPDATE_TODO_MUTATION, {
    onCompleted: () => {
      toast.success('Todo updated')
    onError: (error) => {

  const onSave = (input: UpdateTodoInput, id: EditTodoById['todo']['id']) => {
    updateTodo({ variables: { id, input } })

  return (
    <div className="flex justify-center px-8">
      <table className="container max-w-4xl">
          {todos.map((todo) => {
            if (!todo.completed) {
              return (
                  className={`flex items-center p-4 transition-opacity ${
                    todo.completed && 'opacity-25'
                        onChange={() =>
                          onSave({ completed: !todo.completed }, todo.id)
                        className="rw-input h-4 w-4"
                        errorClassName="rw-input rw-input-error"
                  <td className="flex-1 px-2">{truncate(todo.body)}</td>
                    <nav className="rw-table-actions gap-2">
                        to={routes.editTodo({ id: todo.id })}
                        title={'Edit todo ' + todo.id}
                        className="text-gray-500 hover:text-green-500"
                        <FileEdit />
                        title={'Delete todo ' + todo.id}
                        className="text-gray-500 hover:text-red-500"
                        onClick={() => onDeleteClick(todo.id)}
                        <XCircle />
            } else {
              return (
                <tr key="notodos" className="w-full text-center">
                  No todos. Please add a todo to create a list!

export default TodosList

We can now see our newly designed todos at the root of our application (http://localhost:8910/) as seen below.

And after adding our first todo:

Final root layout of our project app

Next steps?

Now it is time to explore the full power of Redwoodjs and explore some of its features. Some fun ideas would be to implement user authentication to keep those todos separate, update the input styles, and possibly a view for seeing completed todos.

To view the demo repo, https://github.com/deptagency/blog-todoapp-example.


While our example was very simple, you can see how quickly it is to get up and running with functional code using RedwoodJs! Now for some caveats, at the time of this writing I am unable to recommend Redwood in its current state for beginners. Redwood’s development started as an more of an enterprise scaling applications more so than consumer usage so if you are not familiar with the underlying tools (Prisma, GraphQL, Node, React, Postgres), I would encourage you to start with gaining fundamental knowledge in those tools before diving in too deep with Redwood.

The future roadmap includes integrating with React Server components as the React core team continues to update React’s core to utilize these more smoothly.

If you still want more Redwood, I would encourage you to view the outstanding documentation on the Redwood homepage.