Develop a RedwoodJS URL shortener application

Develop a RedwoodJS URL shortener application

Introduction

What is RedwoodJS

Redwood is is built on React, GraphQL, and Prisma, Redwood works with the components and development workflow, but with simple conventions and helpers to make your experience even better, this is opinionated.

To keep it simple, let's use the tag line which they go by.

"Bringing full-stack to the Jamstack"

Redwood is really a vast framework,this blog post will try to cover the most important concepts of it.

https://d33wubrfki0l68.cloudfront.net/b7d16f7f3654fb8572360301e60d76df254a323e/385ec/img/svg/architecture.svg

Image taken from https://jamstack.org/

What are we building now

We will be building a small URL shortener service, we will focus less on UI and functionality aspect of the shortener as a whole and concentrate more on RedwoodJS.

Bird eye view of technologies being used

We will be working with the following technologies in the tutorial

  • RedwoodJS
  • Tailwind
  • GraphQL
  • Prisma

Just to put it in simple words, HOSTURL/:slug will add a view to the local database and redirect to the desired website.

If you are developing locally the HOSTURL would be [localhost:8910](http://localhost:8910) and the :slug can be anything.

Simple example:

[localhost:8910/gilla-twitter](http://localhost:8910/gilla-twitter) should redirect to my twitter profile.

P.S Don't worry even if you don't know anything, our objective will be to cover them below.

Development

In this section we would be going over the development process.

Create Redwood project

As with any modern framework, Redwood comes with a project generator.

Redwood supports both TypeScript and JavaScript, you can use the required language of your choice, for this tutorial we will be going with TypeScript

For JS

yarn create redwood-app ./redwood-url-shortner

Warp

For TS

yarn create redwood-app --typescript redwood-url-shortner

Warp

This will generate the base project for us with sqlite database, which we can swap with just a few changes.

You can follow this to get it up and running locally, for deployment you can use services such as heroku or railway.

In this tutorial we will stick to sqlite database as we won't be performing deployment.

Understanding the high level structure of the application

On the high level you could see two repositories, web and api which are two workspaces in yarn terminology.

In in web we have our Frontend of the application and in api the GraphQL backend resides.

Web Directory

The other files are simple, like the css html and the .ts/.js files.

Now the way yarn workspaces works on installing dependencies are is as follows.

yarn workspace <space name> add <dependency name>

Say for installing axios in web workspace, the command would be

yarn workspace web app axios

Note: rw is shorthand for redwood cli.

Adding Tailwind

Now that we understand the application at a high level, we need to include our custom webpack configuration.

Redwood supports that too, you can check it out here.

Redwood can include the Tailwind CSS in just a single command line code, this takes care of all the production ready configuration for Tailwind CSS.

This is the command to setup the Tailwind, you could see the sample output in the warp link below.

yarn rw setup tailwind

Warp

Please remember to restart your yarn rw dev server after adding tailwind.

Creating new page

We need to generate the page for the home, currently it shows redwood home page.

The following command is used for doing so, you can also find sample output of the terminal.

yarn rw g page app /

Warp

  • g is the shorthand for generate
  • page is used for generating pages
  • app is the name of the components and pages that will be generated.
  • / is the route path.

In the Routes.ts files, the main change will be reflected.

localhost:8910 will now show the home page after this command.

In the AppPageLayout (if generated or else you can generate it)

You can use this simple layout to have good padding around the content.

type AppPageLayoutProps = {
  children?: React.ReactNode
}

const AppPageLayout = ({ children }: AppPageLayoutProps) => {
  return <div className="p-8 h-screen bg-blue-200">{children}</div>
}

export default AppPageLayout

Prisma model for the UrlScheme

Create the prisma model for the URL Shortener, the model contains the following values.

model Url {
  id        Int      @id @default(autoincrement())
  longUrl   String
  slug      String   @unique
  views     Int      @default(0)
  createdAt DateTime @default(now())
}

We will try to keep it as simple as possible, now the migrations need to be applied to the database, for this there is a command in rw cli.

This command is used for creating and applying the migration.

yarn rw prisma migrate dev

Warp

Generating Scaffold

Now this is where the actual Redwood magic starts, as we discussed earlier we will concentrate on generating the scaffold.

yarn rw generate scaffold url

Warp

The above command generates

  • CRUD in the api folder
  • Components, Layouts, Cells for the web folder.
  • GraphQL schema definition files.

If you visit the /urls/new page, you can check the form to add data has already been created, and you can start adding the data.

https://res.cloudinary.com/rohith-gilla/image/upload/v1629722780/Blog/RedwoodJS/Screenshot_2021-08-23_at_6.16.14_PM_mdlln3.png

Once added the data head over to /urls page (you would be automatically redirected here, if not visit it manually in the browser)

The page would look something like this.

https://res.cloudinary.com/rohith-gilla/image/upload/v1629722758/Blog/RedwoodJS/Screenshot_2021-08-23_at_6.15.50_PM_cx8vnp.png

The Show , Edit and Delete function works, the CRUD are already all wired in.

api/src/graphql is something you can spend time exploring, you can also launch the GraphQL playground using localhost:8911/graphql

Understanding Cell Architecture

There is a lot of terminology of Cell that is going around, there are a few definitions on the official docs and other places. To put it in simple words let's understand by a quick example.

For any API call the frontend makes, there is always four main possibilities Success , Empty , Failure and Loading

So we need three different UIs for the following states based on the design requirements.

The Cell is a high order component where you can export 4 components

  • Loading
  • Empty
  • Failure
  • Success

The use of just the cell component for example,<UrlsCell/>

Just usage of this component on the code would be enough, the states and rendering the particular state will be taken care by redwood.

Which means if the API call is success it renders the Success component, when it is loading it renders the Loading component and so o

Writing custom SDLs

For most of the cases the CRUD will be enough, but for a few cases there is a requirement for having custom queries.

We have a need for custom query, we need to find the longUrl which needs to be redirected to from the slug we have.

The CRUD which we already have can get the data based on the id and not slug.

For this

  • Add the required function in the services/ folder

    Here we are dealing with the urls so we add the following in api/src/services/urls/urls.ts file.

      export const findBySlug = ({ slug }: Prisma.UrlWhereUniqueInput) => {
      return db.url.findUnique({ where: { slug } })
      }
    
  • Add the findBySlug in the Schema definition file.

    In the case of us we need to modify the following file api/src/graphql/urls.sdl.ts as

      type Query {
          urls: [Url!]!
          url(id: Int!): Url
          findBySlug(slug: String!): Url // Added
        }
    
  • We need to create a custom cell, because we also need to handle case of incrementing the view count, if you notice our schema definition there is a views field, the FindBySlug will be discussed in the section below.

Creating the FindBySlug cell

As we have done earlier, the scaffold command generates many things which also includes cells.

To generate cel alone without any other side files generated, there is a command to do that.

You can find the command and it's sample output below.

yarn rw g cell FindBySlug

Warp

Once the required files are generated, in the main FindBySlugCell.tsx file.

There is a QUERY variable, this is the query that will be run and based on the result the states will be rendered.

export const QUERY = gql`
  query FindBySlugQuery($id: String!) {
    findBySlug: findBySlug(slug: $id) {
      id
      longUrl
      slug
      views
      createdAt
    }
  }
`

Now for testing this implementation we need to wire this component in the AppPage.tsx file that was generated with the page command we used.

The AppPage.tsx would look similar to this.

import { MetaTags } from '@redwoodjs/web'
import FindBySlugCell from 'src/components/FindBySlugCell'
import AppPageLayout from '../../layouts/AppPageLayoutLayout/AppPageLayoutLayout'
import NewUrlPage from '../Url/NewUrlPage/NewUrlPage'

type AppPageProps = {
  slug?: string
}

const AppPage = ({ slug }: AppPageProps) => {
  return (
    <AppPageLayout>
      <MetaTags title="App" />
      <h1>URL shortner App</h1>
      {slug && <FindBySlugCell id={slug} />}
    </AppPageLayout>
  )
}

export default AppPage

Note that we are taking in props of slug , we take this slug from the URL bar, so this needs to be handled in Routes.tsx file.

<Route path="/{slug:String}" page={AppPage} name="app" />

This line will ensure to load AppPage component on /:slug.

Please note that how well the type checking and safety is, really great stuff.

Now on hitting the url which you have created, the page would look something similar to this.

https://res.cloudinary.com/rohith-gilla/image/upload/v1629723173/Blog/RedwoodJS/Screenshot_2021-08-23_at_6.22.46_PM_c7yzxh.png

Ignore the part of the image where you see New URL form in the page.

But the json data can be seen on the page.

Putting everything together.

Now, we need to add a new count to the views and also redirect the user after that.

Again repeating the drill for adding new custom GraphQL query.

  • Add the following in urls.ts file

      interface UpdateViewArgs extends Prisma.UrlWhereUniqueInput {
        count: number
      }
    
      export const incrementViews = ({ id, count }: UpdateViewArgs) => {
        return db.url.update({
          where: { id },
          data: { views: count },
        })
      }
    
  • Modify the sdl file by adding this

      type Mutation {
          createUrl(input: CreateUrlInput!): Url!
          updateUrl(id: Int!, input: UpdateUrlInput!): Url!
          deleteUrl(id: Int!): Url!
          incrementViews(id: Int!, count: Int!): Url!
        }
    

You can check the incrementViews in the GraphQL playground, attaching image below to show how it looks.

https://res.cloudinary.com/rohith-gilla/image/upload/v1629724109/Blog/RedwoodJS/Screenshot_2021-08-23_at_6.38.18_PM_gpdwxj.png

In the FindBySlugCell.tsx we will be adding another GraphQL mutation.

const INCREMENT_VIEWS = gql`
  mutation IncrementViewMutation($id: Int!, $count: Int!) {
    incrementViews: incrementViews(id: $id, count: $count) {
      id
      longUrl
      slug
      views
    }
  }

// in the success block
const [increment] = useMutation(INCREMENT_VIEWS)
useEffect(() => {
    increment({
      variables: {
        id: findBySlug.id,
        count: findBySlug.views + 1,
      },
    })
        // used for redirection 
    window.location.href = findBySlug.longUrl
  }, [findBySlug.id])

The overall file would look something like this.

import type { FindBySlugQuery } from 'types/graphql'
import { useMutation } from '@redwoodjs/web'
import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web'
import { useEffect } from 'react'

export const QUERY = gql`
  query FindBySlugQuery($id: String!) {
    findBySlug: findBySlug(slug: $id) {
      id
      longUrl
      slug
      views
      createdAt
    }
  }
`

const INCREMENT_VIEWS = gql`
  mutation IncrementViewMutation($id: Int!, $count: Int!) {
    incrementViews: incrementViews(id: $id, count: $count) {
      id
      longUrl
      slug
      views
    }
  }
`

export const Loading = () => <div>Loading...</div>

export const Empty = () => <div>Empty</div>

export const Failure = ({ error }: CellFailureProps) => (
  <div style={{ color: 'red' }}>Error: {error.message}</div>
)

export const Success = ({ findBySlug }: CellSuccessProps<FindBySlugQuery>) => {
  const [increment] = useMutation(INCREMENT_VIEWS)
  useEffect(() => {
    increment({
      variables: {
        id: findBySlug.id,
        count: findBySlug.views + 1,
      },
    })
    window.location.href = findBySlug.longUrl
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [findBySlug.id])

  return <div>{JSON.stringify(findBySlug)}</div>
}

Adding the form to the Home page.

By just adding the <NewUrlPage /> which was generated for us can directly used as an component.

The overall AppPage.tsx component will look something like

import { MetaTags } from '@redwoodjs/web'
import FindBySlugCell from 'src/components/FindBySlugCell'
import AppPageLayout from '../../layouts/AppPageLayoutLayout/AppPageLayoutLayout'
import NewUrlPage from '../Url/NewUrlPage/NewUrlPage'

type AppPageProps = {
  slug?: string
}

const AppPage = ({ slug }: AppPageProps) => {
  return (
    <AppPageLayout>
      <MetaTags title="App" />
      <h1>URL shortner App</h1>
      {slug && <FindBySlugCell id={slug} />}
      <div className="m-4">
        <NewUrlPage />
      </div>
    </AppPageLayout>
  )
}

export default AppPage

The application would be similar to this.

https://res.cloudinary.com/rohith-gilla/image/upload/v1629723106/Blog/RedwoodJS/Screenshot_2021-08-23_at_6.21.36_PM_tqrwfs.png

Code

Github Repository

The application code can be found in the Github repo.

GitHub - Rohithgilla12/redwood-url-shortner

Next Steps

Wohoo, we have successfully created a Redwood JS application, thought this can be improved a lot, but the main objective of learning how to use RedwoodJS was covered.

Deployment

Currently Redwood supports Netlify , Vercel, AWS-Serverless , Render as official supported targets.

yarn rw deploy <place> will create ready to deploy version of the project, also please note that backend needs to be deployed separately except for Render

There is a detailed explanation about different deploy methods here.

Now if you want an opinionated suggestion about deployment.

  • Choose Netlify or Vercel for the Frontend deployment, if you don't have any personal preference choose the one which has more credits left :)
  • Choose Heroku / Railway for the database, you can summon a Postgres db within a few clicks. Railway has pay as you go structure, you would be able to handle a decent amount of traffic in free tier itself, Heroku you get 10k records in the DB in the free tier.

Feeling Adventurous?

  • Add Authentication to the existing application.
  • Provide User dashboard, show the graph of views per day.
  • Try to use the existing API and build a mobile application from it, please use this as reference for consuming API.

More reading

  • Roadmap is worth checking out, some really cool stuff are coming out.
  • Official Docs can be found here.
  • Official Learning website can be find here.
  • Deep dive of API folder can be find here.
  • Deep dive of Web folder can be find here.
  • Deep dive on cells.

Thanks

Rohith Gilla