Develop URL shortener application with RedwoodJS
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.
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
For TS
yarn create redwood-app --typescript redwood-url-shortner
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.
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
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 /
-
g
is the shorthand forgenerate
-
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
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
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.
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.
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/
folderHere we are dealing with the
urls
so we add the following inapi/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, theFindBySlug
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
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.
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.
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.
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
orVercel
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
Top comments (2)
Fascinating! How long did that take you to build?
Thanks
It took 3-4 hours I guess, the time was more because after every command I took the terminal output and saved it.
If you open any warp link in the post you will see the output.
But I think writing the post took almost same as developing I guess.