Redwood.js: The Full-Stack React Framework
PROGRAMMING LANGUAGES Jan. 15, 2026, 11:30 p.m.

Redwood.js: The Full-Stack React Framework

RedwoodJS is a full‑stack JavaScript framework that stitches together React, GraphQL, Prisma, and serverless functions into a cohesive developer experience. It lets you write both the UI and the API in the same repository, share TypeScript types, and deploy to platforms like Netlify or Vercel with a single command. In this article we’ll explore Redwood’s core concepts, walk through two practical examples, and share pro tips to help you get the most out of the framework.

Why RedwoodJS Stands Out

Unlike traditional React projects where you manually wire up a backend, Redwood gives you a convention‑driven structure that enforces clear separation of concerns. Pages live in ./web/pages, data fetching happens in ./web/components via cells, and server‑side logic resides in ./api as GraphQL resolvers. This “frontend‑first” philosophy means you can start building UI components immediately, then let Redwood generate the API scaffolding for you.

Redwood also embraces the Jamstack mindset: each GraphQL resolver runs as an isolated serverless function, which translates to automatic scaling, low latency, and minimal ops overhead. Combined with Prisma’s type‑safe database layer, you get end‑to‑end type safety without sacrificing flexibility.

Core Concepts at a Glance

Pages and Routes

Pages are simple React components placed under ./web/pages. Redwood automatically creates routes based on the file name, so HomePage.js becomes / and AboutPage.js becomes /about. You can also nest routes using the Router component in Routes.js.

Cells – Data Fetching Made Declarative

A cell is a self‑contained component that declares a GraphQL query, loading state, empty state, error handling, and success rendering—all in one file. This pattern eliminates boilerplate and keeps data concerns close to the UI.

Services – Business Logic on the Server

Services live in ./api/src/services and expose functions that become GraphQL resolvers. By default, Redwood generates CRUD services for each Prisma model, but you can extend them with custom logic, side‑effects, or third‑party integrations.

Prisma – Type‑Safe Database Access

Prisma serves as Redwood’s ORM. Your schema lives in ./api/prisma/schema.prisma, and Redwood generates TypeScript types for every model. This means you get compile‑time guarantees that your queries match the database shape.

Getting Started: Setting Up a New Redwood Project

First, install the Redwood CLI globally (requires Node ≥18):

npm install -g redwoodjs/cli

Then create a new project called todo-app and navigate into it:

redwood new todo-app
cd todo-app

The CLI scaffolds a monorepo with web (React) and api (GraphQL) workspaces, installs dependencies, and runs yarn rw dev to start both the frontend and backend simultaneously.

Example 1: Building a Simple Todo Application

We’ll start with a minimal CRUD todo list that demonstrates Redwood’s end‑to‑end workflow—from database schema to UI.

Step 1: Define the Prisma Model

Open api/prisma/schema.prisma and add a Todo model:

model Todo {
  id          Int      @id @default(autoincrement())
  title       String
  completed   Boolean  @default(false)
  createdAt   DateTime @default(now())
}

Run the migration to create the table in SQLite (the default dev database):

yarn rw prisma migrate dev

Step 2: Generate the Service

Redwood can auto‑generate a service with full CRUD operations:

yarn rw g service todo

This creates api/src/services/todos/todos.ts with functions like todos(), todo({id}), createTodo(), etc., and automatically registers them as GraphQL resolvers.

Step 3: Create the Cell

Now we need a UI component that fetches and displays todos. Use the generator:

yarn rw g cell Todos

The generator creates web/src/components/Todos/TodosCell.js with a GraphQL query and placeholder UI for loading, empty, error, and success states. Replace the success block with a list:

export const SUCCESS = ({ todos }) => {
  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>
          <input
            type="checkbox"
            checked={todo.completed}
            onChange={() => toggleTodo({ id: todo.id, completed: !todo.completed })}
          />
          {todo.title}
        </li>
      ))}
    </ul>
  )
}

Notice the toggleTodo mutation we’ll add next.

Step 4: Add a Mutation for Toggling Completion

Create a new GraphQL mutation in api/src/graphql/todos.sdl.js:

export const schema = gql`
  type Mutation {
    toggleTodo(id: Int!, completed: Boolean!): Todo! @requireAuth
  }
`

Implement the resolver in api/src/services/todos/todos.js:

export const toggleTodo = async ({ id, completed }) => {
  return db.todo.update({
    where: { id },
    data: { completed },
  })
}

Now we can call this mutation from the cell’s success UI using Redwood’s useMutation hook:

import { useMutation } from '@redwoodjs/web'

export const SUCCESS = ({ todos }) => {
  const [toggleTodo] = useMutation(gql`
    mutation ToggleTodo($id: Int!, $completed: Boolean!) {
      toggleTodo(id: $id, completed: $completed) {
        id
        completed
      }
    }
  `)

  // ...rest of the component as shown earlier
}

Step 5: Wire the Cell into a Page

Create a page that renders the TodosCell:

yarn rw g page Todos

Edit web/src/pages/TodosPage/TodosPage.js to include the cell:

import TodosCell from 'src/components/Todos/TodosCell'

const TodosPage = () => {
  return (
    <div>
      <h1>My Todos</h1>
      <TodosCell />
    </div>
  )
}

export default TodosPage

Run yarn rw dev, navigate to /todos, and you’ll see a fully functional todo list with live GraphQL updates.

Pro tip: Enable Redwood’s autoImport feature in redwood.toml to skip manual imports for generated components. This reduces boilerplate and keeps your pages tidy.

Example 2: Adding Authentication with Netlify Identity

Most real‑world apps need user authentication. Redwood ships with first‑class support for Netlify Identity, Auth0, Firebase, and more. We’ll integrate Netlify Identity because it works out‑of‑the‑box with Redwood’s serverless deployment model.

Step 1: Enable Auth in the Project

Run the auth generator and select Netlify Identity:

yarn rw g auth netlify

The CLI adds an auth directory under web/src, creates a useAuth hook, and injects a NetlifyProvider into App.js. It also adds a requireAuth directive to the GraphQL schema, protecting any resolver you tag.

Step 2: Protect the Todo Routes

Update web/src/Routes.js to wrap the TodosPage with Private:

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

const Routes = () => {
  return (
    <Router>
      <Route path="/" page={HomePage} name="home" />
      <Private unauthenticated="home">
        <Route path="/todos" page={TodosPage} name="todos" />
      </Private>
    </Router>
  )
}

Now only logged‑in users can access the todo list.

Step 3: Add Sign‑Up / Sign‑In UI

Redwood’s auth scaffolding includes LoginPage and SignupPage. You can customize them or create a unified AuthLayout:

import { useAuth } from 'src/auth'
import { Link, routes } from '@redwoodjs/router'

const AuthLayout = ({ children }) => {
  const { isAuthenticated, logOut } = useAuth()

  return (
    <div>
      <nav>
        {isAuthenticated ? (
          <button onClick={logOut}>Log out</button>
        ) : (
          <>
            <Link to={routes.login}>Log in</Link> |{' '}
            <Link to={routes.signup}>Sign up</Link>
          </>
        )}
      </nav>
      <main>{children}</main>
    </div>
  )
}

Wrap your pages with AuthLayout in Routes.js to provide a consistent navigation bar.

Step 4: Deploy to Netlify

Redwood’s yarn rw deploy netlify command builds both the web and api sides, uploads the static assets, and creates Netlify Functions for each GraphQL resolver. After the first deployment, enable Netlify Identity in the site dashboard, and you’re ready to authenticate real users.

Pro tip: Use Netlify’s “Git Gateway” mode to let Redwood’s useAuth hook handle token refresh automatically. This eliminates the need for manual JWT handling on the client.

Advanced Features Worth Exploring

GraphQL Fragments & Type Safety

Because Redwood generates TypeScript types for every GraphQL operation, you can import TodoFragment directly into components:

import { TodoFragment } from 'types/graphql'

type Props = {
  todo: TodoFragment
}

This ensures your UI always receives the fields you request, catching mismatches at compile time.

Side Effects with Services

Services are an ideal place for side effects such as sending emails, calling third‑party APIs, or publishing to a message queue. For example, after creating a new todo you might notify a Slack channel:

import { db } from 'src/lib/db'
import { sendSlackMessage } from 'src/lib/slack'

export const createTodo = async ({ input }) => {
  const todo = await db.todo.create({ data: input })
  await sendSlackMessage(`🗒️ New todo: ${todo.title}`)
  return todo
}

Testing Redwood Apps

Redwood ships with Jest for unit tests and Cypress for end‑to‑end tests. A typical unit test for a service looks like this:

import { createTodo } from './todos'
import { db } from 'src/lib/db'

describe('createTodo service', () => {
  it('creates a todo and returns it', async () => {
    const input = { title: 'Test Todo' }
    const result = await createTodo({ input })
    expect(result.title).toBe('Test Todo')
    expect(result.id).toBeDefined()
    // Clean up
    await db.todo.delete({ where: { id: result.id } })
  })
})

For E2E, Redwood provides a Cypress command cy.login() that works with any auth provider you’ve configured, making it trivial to write tests that span the full stack.

Performance & Scaling Considerations

Since each GraphQL resolver runs as an isolated serverless function, cold starts can affect latency on low‑traffic routes. Mitigate this by grouping related resolvers into a single function using the api/src/functions/graphql.js entry point, or by enabling “warm” functions on your hosting platform.

Redwood also supports Prisma’s connection pooling via the pgBouncer proxy when using PostgreSQL in production. This reduces the overhead of opening a new database connection per request, which is crucial for high‑throughput APIs.

Pro tip: Enable Redwood’s experimental.graphqlCache flag to cache identical GraphQL queries at the edge (e.g., Cloudflare Workers). This can shave off milliseconds on read‑heavy workloads.

Real‑World Use Cases

  • Marketplace platforms: Combine Redwood’s GraphQL API with Stripe webhooks to build buyer/seller flows without writing separate backend services.
  • Internal dashboards: Leverage Prisma’s introspection to generate admin UIs that automatically adapt to schema changes.
  • Jamstack blogs with dynamic comments: Store comments in a PostgreSQL database, expose them via Redwood’s GraphQL, and render them client‑side with React.

Companies like Fireside and Skylight Labs have adopted Redwood for rapid MVP delivery, citing the unified type system and zero‑config deployment as major productivity boosters.

Best Practices Checklist

  1. Keep your Prisma schema as the single source of truth; never duplicate model definitions.
  2. Use cells for every data‑fetching component to standardize loading, error, and empty states.
  3. Guard sensitive resolvers with @requireAuth and role‑based directives.
  4. Write unit tests for services and integration tests for cells to catch regressions early.
  5. Leverage Redwood’s built‑in generate commands to stay consistent with conventions.

Conclusion

RedwoodJS offers a compelling blend of convention, type safety, and serverless scalability that makes full‑stack React development feel like a single, cohesive experience. By embracing cells, services, and Prisma, you can move from UI prototype to production‑ready API in minutes, while still retaining the flexibility to inject custom business logic and third‑party integrations. Whether you’re building a simple todo app or a complex marketplace, Redwood’s opinionated architecture helps you stay productive, maintainable, and ready for modern

Share this article