Build a full-stack Jamstack Application

Giridhar - Oct 16 '21 - - Dev Community

Many modern approaches for designing flexible and scalable web applications have become popular as browsers have evolved. Jamstack is one of these cutting-edge techniques to develop lightning-fast web apps. Jamstack combines the capabilities of JavaScript, APIs, and Markup to create modern, browser-friendly web apps, which can function without any web servers. In this tutorial, you'll create a custom scrapbook application that allows you to store and access your daily memories from anywhere in the world.

Check out the live demo of the application you'll create.

scrapbook application demo

Final demo | Source code

Before diving into this tutorial:

  • You should have a basic understanding of how to use ES6 features of JavaScript.
  • You should have beginner-level experience of using React and React Hooks.
  • It would be advantageous if you have prior expertise with any design system in React.

Tech Stack

Jamstack is unique because it allows you to integrate several technologies to create a full-stack application. You will use the following technologies to create this scrapbook:

  • Next.js is a React framework that extends the amazing powers of React for creating multiple page apps easily. Without using any backend frameworks like Express, you may use Next.js and its serverless functions to develop your app's frontend and backend.
  • Chakra-UI provides a plethora of stylish and accessible react components for styling your web application.
  • You'll be using Airtable as a database solution for this application. Airtable is a spreadsheet/database hybrid with a fantastic API for integrating it into your application.
  • Cloudinary is a cloud media management platform where you'll upload photos of your scrapbook.
  • Auth0 enables you to integrate a user authentication system into your app. It uses OAuth 2.0 and provides a secure OAuth layer for your app.

Auth0 and Cloudinary both offer free plans. You can create an account and use it for free to develop this application.

Table of Contents

  • Getting Started
  • Connecting Airtable to your app
  • Integrate Airtable with Next.js Serverless functions
  • Uploading files to Cloudinary
  • Create React Context for Posts
  • Setup Authentication with Auth0
  • Next Steps

Getting Started

Fork this starter code sandbox template and get ready for coding the scrapbook application. If you prefer to use local development, you should have Node and NPM installed.

Running the following command creates a Next.js and Chakra-UI starter application with no configuration.

npx create-next-app --example with-chakra-ui <app-name>
# or
yarn create next-app --example with-chakra-ui <app-name>
Enter fullscreen mode Exit fullscreen mode

Now, head over to Airtable and create a free account for yourself or log in if you already have one. After logging in, create a new base(database) from scratch by clicking on Add a base button and give it a meaningful name. It makes a new base with some primary fields. You can customize the table by double-clicking on the columns. Start customizing the table name to posts and add the following columns:

  • image - URL
  • caption - single-line text
  • cloudinaryId - single-line text
  • userId - single-line text
  • date - Date

The base should look something like this:

starter table with the columns: image, caption, cloudinaryId, userId and date

Next, navigate to Airtable API and select the base you'd like to integrate. Create a .env file in the root directory and add some secrets as environment variables. For connecting Airtable to our app, you'll need the following secrets in the code.

  • API_KEY: the Airtable API key. You can find it within the documentation(by checking the "Show API key" box in the top right) or on your account page.
  • BASE_ID: the id of the base you want to integrate. You can find it on the documentation page.
  • TABLE_NAME: the name of the table in that base (you can use a single base for multiple tables).

airtable api page with secrets

/.env

AIRTABLE_API_KEY=<your-airtable-api-key>
AIRTABLE_BASE_ID=<your-airtable-base-id>
AIRTABLE_TABLE_NAME=<your-airtable-table-name>
Enter fullscreen mode Exit fullscreen mode

Install the Airtable JavaScript SDK to work with Airtable inside a Next.js application.

npm install airtable
Enter fullscreen mode Exit fullscreen mode

Also, install the following dependencies to finish setting up your dev environment.

npm install @auth0/nextjs-auth0 multer next-connect cloudinary
Enter fullscreen mode Exit fullscreen mode

For using Auth0 and Cloudinary in your application, you need Auth0 Next.js SDK (@auth0/nextjs-auth0) and Cloudinary SDK (cloudinary) respectively. Multer is for handling file inputs and Next-connect is for dealing with middleware in Next.js API Routes.

Connecting Airtable to Your App

Now, create a new folder /utils inside the /src folder and add a new file Airtable.js. The code below connects your app to Airtable, retrieves the data. By default, the Airtable returns unnecessary data. The minifyRecords function returns the minified version of the record with necessary data.

/utils/Airtable.js

const Airtable = require('airtable')

// Authenticate
Airtable.configure({
  apiKey: process.env.AIRTABLE_API_KEY
})

// Initialize a base
const base = Airtable.base(process.env.AIRTABLE_BASE_ID)

// Reference a table
const table = base(process.env.AIRTABLE_TABLE_NAME)

// To get an array of  meaningful records
const minifyRecords = (records) =>
  records.map((record) => ({
    id: record.id,
    fields: record.fields
  }))

export { table, minifyRecords }
Enter fullscreen mode Exit fullscreen mode

Integrate Airtable with Next.js Serverless Functions

Using API routes, you can construct your own API in Next.js. Any file you add inside the /pages/api folder will be treated as an API endpoint(/api/*) rather than a regular route. You can handle any request that hits the endpoint using serverless functions. Let's create an API endpoint to retrieve Airtable records.

/src/pages/api/getPosts.js

// For retreving posts from Airtable
import { table, minifyRecords } from '../../utils/Airtable'

export default async (req, res) => {
  try {
    // get records from airtable
    const records = await table.select({}).firstPage()

    // send an array of minified records as a response
    const minfiedRecords = minifyRecords(records)
    res.status(200).json(minfiedRecords)
  } catch (error) {
    console.error(error)
    res.status(500).json({ msg: 'Something went wrong! 😕' })
  }
}
Enter fullscreen mode Exit fullscreen mode

Setting up Cloudinary

After uploading the scrapbook photos to Cloudinary, you will need to store the secure_url and public_id to the Airtable database. Go to your Cloudinary Dashboard, copy the following secrets and paste them into the .env file.

cloudinary dashboard with cloudname, api key and api secret

/.env

...
CLOUDINARY_CLOUD_NAME=<your-cloudinary-cloud-name>
CLOUDINARY_API_KEY=<your-cloudinary-api-key>
CLOUDINARY_API_SECRET=<your-cloudinary-api-secret>
Enter fullscreen mode Exit fullscreen mode

After adding the environment variables, create a new file cloudinary.js inside /utils directory to setup cloudinary.

/utils/cloudinary.js

import { v2 as cloudinary } from 'cloudinary'

cloudinary.config({
  cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
  api_key: process.env.CLOUDINARY_API_KEY,
  api_secret: process.env.CLOUDINARY_API_SECRET
})

export default cloudinary
Enter fullscreen mode Exit fullscreen mode

Uploading Files to Cloudinary

You can upload photos to Cloudinary using the uploader.upload method of Cloudinary SDK. By default, Next.js API routes cannot handle file input. So you're going to use the multer, that will append files to the request. You'll also use next-connect to deal with the middleware functions(Learn more). Create a new file, createPost.js inside /api for uploading images.

/src/pages/api/createPost.js

// For creating a new record in Airtable
import nextConnect from 'next-connect'
import multer from 'multer'
import path from 'path'
import { table, minifyRecords } from '../../utils/Airtable'
import cloudinary from '../../utils/cloudinary'

// multer config
const upload = multer({
  storage: multer.diskStorage({}),
  fileFilter: (req, file, cb) => {
    let ext = path.extname(file.originalname)
    if (ext !== '.jpg' && ext !== '.jpeg' && ext !== '.png') {
      cb(new Error('File type is not supported'), false)
      return
    }
    cb(null, true)
  }
})

const createPost = nextConnect({
  onError(error, req, res) {
    res.status(501).json({ error: error.message })
  }
})

// Adds the middleware to Next-Connect
// this should be the name of the form field
createPost.use(upload.single('image'))

createPost.post(async (req, res) => {
  // Uploading file to cloudinary
  const result = await cloudinary.uploader.upload(req.file.path)
  // Create a new record with required fields
  const post = {
    image: result.secure_url,
    caption: req.body.caption,
    cloudinaryId: result.public_id,
    userId: req.body.userId
  }

  // Create a record with the above fields in Airtable
  // the 'create' method accepts and returns an array of records
  const newRecords = await table.create([{ fields: post }])
  res.status(200).json(minifyRecords(newRecords)[0])
})

export default createPost

export const config = {
  api: {
    bodyParser: false
  }
}
Enter fullscreen mode Exit fullscreen mode

Try using Postman or something similar to test these endpoints. If you run into any problems, compare with the code snippet provided or try troubleshooting using the internet.

Displaying Scrapbook Posts

Now that you have the API, let's design an interface to display the data in our Next.js application. You can use the getServerSideProps function of Next.js with serverside rendering to display data that is coming from an API. Every time the page is rendered, Next.js runs the code contained within this function.

You can learn more about Next.js server-side rendering here.

Add the following code to the index.js file. The posts will be displayed as cards in a grid.

/src/pages/index.js

import { Container } from '../components/Container'
import { Flex, Grid, Text } from '@chakra-ui/react'
import Card from '../components/Card'

export default function Index({ initialPosts }) {
  return (
    <Container minH="100vh">
      <Flex flexDirection="column" justifyContent="center" alignItems="center">
        <Flex w="100%" flexDirection="column" my={8}>
          {!initialPosts.length ? (
            <Flex
              h={['30vh', '50vh']}
              w="100%"
              justifyContent="center"
              alignItems="center"
            >
              <Text fontSize={['2xl', '3xl']} opacity="0.2">
                No Posts Added
              </Text>
            </Flex>
          ) : (
            <Grid
              templateColumns={[
                'repeat(1, 1fr)',
                'repeat(2, 1fr)',
                'repeat(3, 1fr)'
              ]}
              gap={6}
              m="0 auto"
              w={['100%', '90%', '85%']}
            >
              {initialPosts.map((post) => {
                return <Card post={post.fields} key={post.id} />
              })}
            </Grid>
          )}
        </Flex>
      </Flex>
    </Container>
  )
}

export async function getServerSideProps(context) {
  try {
    const res = await fetch('http://localhost:3000/api/getPosts')
    return {
      props: {
        initialPosts: await res.json()
      }
    }
  } catch (error) {
    console.log(error)
    return {
      props: {
        err: 'Something went wrong 😕'
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

/src/components/Card.js

import { Box, Text, useColorModeValue } from '@chakra-ui/react'

const Card = ({ post }) =>
  post ? (
    <Box
      w="100%"
      p={4}
      flex="1"
      borderWidth="1px"
      bg={useColorModeValue('white', 'gray.800')}
      borderColor={useColorModeValue('gray.100', 'gray.700')}
      rounded="lg"
      shadow="md"
    >
      <Text textAlign="right" fontSize="sm" fontWeight="bold" mb={4}>
        {new Date(post.date).toString().substr(0, 15)}
      </Text>
      <a href={post.image} target="_blank" rel="noopener noreferrer">
        <img src={post.image} alt={post.cloudinaryId} loading="lazy" />
      </a>
      <Text fontSize="md" my={4} noOfLines={[3, 4, 5]} isTruncated>
        {post.caption}
      </Text>
    </Box>
  ) : null

export default Card
Enter fullscreen mode Exit fullscreen mode

Create React Context for Posts

Create a React context for Posts to share the state of posts across other components. Create a new context folder inside /src and add a new file posts.js.

/src/context/posts.js

import { createContext, useState } from 'react'

const PostsContext = createContext()

const PostsProvider = ({ children }) => {
  const [posts, setPosts] = useState([])

  const addPost = async (formData) => {
    try {
      // sending the form data
      const res = await fetch('/api/createPost', {
        method: 'POST',
        body: formData
      })
      const newPost = await res.json()

      // updating the posts state
      setPosts((prevPosts) => [newPost, ...prevPosts])
    } catch (error) {
      console.error(error)
    }
  }

  return (
    <PostsContext.Provider
      value={{
        posts,
        setPosts,
        addPost
      }}
    >
      {children}
    </PostsContext.Provider>
  )
}

export { PostsContext, PostsProvider }
Enter fullscreen mode Exit fullscreen mode

Wrap the app around the PostsProvider to use this context in your application.

/src/pages/_app.js

import { PostsProvider } from '../context/posts'
import theme from '../utils/theme'

function MyApp({ Component, pageProps }) {
  return (
    <PostsProvider>
      <ChakraProvider resetCSS theme={theme}>
        ...
      </ChakraProvider>
    </PostsProvider>
  )
}

export default MyApp
Enter fullscreen mode Exit fullscreen mode

Now, update the posts state to the initialPosts inside the index.js file. At this point, you can see the cards populated with data from Airtable.

/src/pages/index.js

import { useContext, useEffect } from 'react'
import { PostsContext } from '../context/posts'

export default function Index({ initialPosts }) {
  const { posts, setPosts } = useContext(PostsContext)

  useEffect(() => {
    setPosts(initialPosts)
  }, [initialPosts, setPosts])

  ...
}
Enter fullscreen mode Exit fullscreen mode

Creating a Form to Add Post

Create a form to add posts from a webpage. Import the addPost function you created in context to submit the form data. Before uploading the file, you should include a preview of the uploaded photo. See more about handling file input in JavaScript. The toast in Chakra-UI is something that displays a message in a separate modal. In this component, you'll use a toast to show the success message.

/src/components/AddPost.js

import { useContext, useRef, useState } from 'react'
import {
  Modal,
  ModalOverlay,
  ModalContent,
  ModalHeader,
  ModalFooter,
  ModalBody,
  ModalCloseButton,
  Button,
  FormControl,
  FormLabel,
  Input,
  useDisclosure,
  useToast
} from '@chakra-ui/react'
import { PostsContext } from '../context/posts'

export const AddPost = ({ children }) => {
  const [image, setImage] = useState()
  const [caption, setCaption] = useState('')
  const [previewSource, setPreviewSource] = useState('')
  const [fileInputState, setFileInputState] = useState('')

  const { addPost } = useContext(PostsContext)

  const { isOpen, onOpen, onClose } = useDisclosure()
  const toast = useToast()
  const initialRef = useRef()

  const handleFileInput = (e) => {
    const file = e.target.files[0]
    setPreviewSource(URL.createObjectURL(file))
    setImage(file)
    setFileInputState(e.target.value)
  }

  const handleSubmit = (e) => {
    e.preventDefault()
    if (!image) return

    let formData = new FormData()
    formData.append('image', image)
    formData.append('caption', caption)

    addPost(formData)

    toast({
      title: 'Hurray!!! 🎉',
      description: 'Post added ✌',
      status: 'success',
      duration: 1500,
      isClosable: true
    })
    onClose()

    setCaption('')
    setFileInputState('')
    setPreviewSource('')
  }

  return (
    <>
      <Button
        fontWeight="medium"
        size="md"
        colorScheme="yellow"
        _active={{
          transform: 'scale(0.95)'
        }}
        onClick={onOpen}
      >
        {children}
      </Button>
      <Modal initialFocusRef={initialRef} isOpen={isOpen} onClose={onClose}>
        <ModalOverlay />
        <ModalContent>
          <ModalHeader fontWeight="bold">Add Post</ModalHeader>
          <ModalCloseButton />
          <form onSubmit={handleSubmit}>
            <ModalBody pb={6}>
              <FormControl>
                <FormLabel>Photo</FormLabel>
                <input
                  type="file"
                  name="image"
                  ref={initialRef}
                  onChange={handleFileInput}
                  value={fileInputState}
                  required
                />
              </FormControl>

              {previewSource && (
                <img
                  src={previewSource}
                  alt="chosen"
                  height="300px"
                  width="300px"
                  style={{ margin: '15px auto' }}
                />
              )}

              <FormControl mt={4}>
                <FormLabel>Caption</FormLabel>
                <Input
                  placeholder="Caption goes here..."
                  type="text"
                  value={caption}
                  onChange={(e) => setCaption(e.target.value)}
                />
              </FormControl>
            </ModalBody>

            <ModalFooter>
              <Button mr={4} onClick={onClose}>
                Cancel
              </Button>
              <Button type="submit" colorScheme="yellow" mr={3}>
                Create
              </Button>
            </ModalFooter>
          </form>
        </ModalContent>
      </Modal>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

You'll be able to test the AddPost component after creating the navbar. Your Navbar will have a title on the left, Add post, login/logout and darkmode toggle buttons on the right. Go ahead and create a new file Navbar.js in /components folder.

/src/components/Navbar.js

import Link from 'next/link'
import { Button } from '@chakra-ui/button'
import { Flex, Text } from '@chakra-ui/layout'
import { DarkModeSwitch } from './DarkModeSwitch'
import { AddPost } from './AddPost'

const Navbar = () => {
  return (
    <Flex
      justifyContent="space-between"
      w="80%"
      flexDirection={['column', 'row']}
      m={4}
    >
      <Text mb={[4, 0]} textAlign="center" fontWeight="bold" fontSize="2xl">
        @Scrapbook
      </Text>
      <Flex justifyContent="space-between">
        <AddPost>Add Post</AddPost>
        <a href="/api/auth/login">
          <Button variant="solid" colorScheme="blue" mx={3} size="md">
            Login
          </Button>
        </a>
        <DarkModeSwitch />
      </Flex>
    </Flex>
  )
}

export default Navbar
Enter fullscreen mode Exit fullscreen mode

At this point, you'll be able to add and display your scrapbook posts. Let's add authentication using Auth0.

Setup Authentication with Auth0

If you are already an Auth0 user login to your account or create a free one today. Create a new Regular web application. You'll use the auth0-next.js sdk to connect Auth0 with your application. Select Next.js when it asks what technology you're using. The following secrets are required to configure Auth0 with your Next.js application. Go to the Settings tab and add the new Auth0 secrets to the .env file.

Auth0 application setting page with the required CLIENT_ID and CLIENT_SECRET

/.env

...
AUTH0_SECRET=<any secret string of length 32>
AUTH0_BASE_URL='http://localhost:3000'<Your application base URL>
AUTH0_ISSUER_BASE_URL=<URL of your tenant domain>
AUTH0_CLIENT_ID=<Your application's client Id>
AUTH0_CLIENT_SECRET=<Your application's client secret>
Enter fullscreen mode Exit fullscreen mode

If you scroll down a little in the Settings tab, you will find a section Application URIs. Add the following callback URLs. Add the base URL of your application if you're using codesandbox.

Create a new file auth/[...auth0].js inside the /pages/api directory. Any route you add inside brackets([]) will be treated as dynamic route.

/src/pages/api/auth/[...auth0].js

import { handleAuth } from '@auth0/nextjs-auth0'

export default handleAuth()
Enter fullscreen mode Exit fullscreen mode

This generates required routes for authentication(/login, /logout and /callback). Under the hood, Auth0 manages the user's authentication state using React Context.
Wrap the inner components of the /pages/_app.js file with the UserProvider to use the useUser hook provided by Auth0 in the entire application.

/src/pages/_app.js

import { UserProvider } from '@auth0/nextjs-auth0'

function MyApp({ Component, pageProps }) {
  return (
    <UserProvider>
      <PostsProvider> ... </PostsProvider>
    </UserProvider>
  )
}

export default MyApp
Enter fullscreen mode Exit fullscreen mode

Inside /index.js, you can now use the useUser hook to retrieve user information. Pass the user as a prop to the Navbar component to add login/logout functionality. Also, let's display You have to log in if the user is not logged in.

/src/pages/index.js

...
import { useUser } from '@auth0/nextjs-auth0'

export default function Index({ initialPosts }) {
  const { posts, setPosts } = useContext(PostsContext)
  const { user, error, isLoading } = useUser()

  useEffect(...)

  if (isLoading) return <div>Loading...</div>
  if (error) return <div>{error.message}</div>

  return (
    <Flex flexDirection="column" justifyContent="center" alignItems="center">
      <Navbar user={user} />
      {user ? (
        <Flex w="100%" flexDirection="column" my={8}>
          {!posts.length ? (
            <Flex
              h={['30vh', '50vh']}
              w="100%"
              justifyContent="center"
              alignItems="center"
            >
              <Text fontSize={['2xl', '3xl']} opacity="0.2">
                No Posts Added
              </Text>
            </Flex>
          ) : (
            <Grid
              templateColumns={[
                'repeat(1, 1fr)',
                'repeat(2, 1fr)',
                'repeat(3, 1fr)'
              ]}
              gap={6}
              m="0 auto"
              w={['90%', '85%']}
            >
              {posts.map((post) => {
                console.log(post)
                return <Card post={post.fields} key={post.id} />
              })}
            </Grid>
          )}
        </Flex>
      ) : (
        <Flex
          h={['30vh', '50vh']}
          w="100%"
          justifyContent="center"
          alignItems="center"
        >
          <Text fontSize={['2xl', '3xl']} opacity="0.2">
            You have to login
          </Text>
        </Flex>
      )}
    </Flex>
  )
}

export async function getServerSideProps(context) { ... }
Enter fullscreen mode Exit fullscreen mode

Update the Navbar.js for logging in and out a user. Also, you should make sure that only a logged-in user can add scrapbook posts.

/src/components/Navbar.js

import { Button } from '@chakra-ui/button'
import { Flex, Text } from '@chakra-ui/layout'
import { DarkModeSwitch } from './DarkModeSwitch'
import { AddPost } from './AddPost'

const Navbar = ({ user }) => {
  return (
    <Flex
      justifyContent="space-between"
      w="80vw"
      flexDirection={['column', 'row']}
      m={4}
    >
      <Text mb={[4, 0]} textAlign="center" fontWeight="bold" fontSize="2xl">
        @Scrapbook
      </Text>
      <Flex justifyContent="space-between">
        {user && <AddPost>Add Post</AddPost>}
        {user ? (
          <a href="/api/auth/logout">
            <Button variant="solid" colorScheme="blue" mx={4} size="md">
              Logout
            </Button>
          </a>
        ) : (
          <a href="/api/auth/login">
            <Button variant="solid" colorScheme="blue" mx={4} size="md">
              Login
            </Button>
          </a>
        )}
        <DarkModeSwitch />
      </Flex>
    </Flex>
  )
}

export default Navbar
Enter fullscreen mode Exit fullscreen mode

Add Authentication to API Routes

Only authenticated users should be able to access the API. You must also associate each post with a specific user and display just the posts that belong to that person. To obtain user information, Auth0 provides withApiAuthRequired and getSession.

Update the API routes as follows:

/pages/api/getPost.js

// For retreving posts from Airtable
import { getSession, withApiAuthRequired } from '@auth0/nextjs-auth0'
import { table, minifyRecords } from '../../utils/Airtable'

export default withApiAuthRequired(async (req, res) => {
  const { user } = await getSession(req, res)

  try {
    const records = await table
      .select({ filterByFormula: `userId= '${user.sub}'` })
      .firstPage()
    const minfiedItems = minifyRecords(records)
    res.status(200).json(minfiedItems)
  } catch (error) {
    console.error(error)
    res.status(500).json({ msg: 'Something went wrong! 😕' })
  }
})
Enter fullscreen mode Exit fullscreen mode

/pages/api/createPost.js

import nextConnect from 'next-connect'
import multer from 'multer'
import { table, minifyRecords } from '../../utils/Airtable'
import cloudinary from '../../utils/cloudinary'
import { getSession, withApiAuthRequired } from '@auth0/nextjs-auth0'

// multer config
const upload = multer({
  storage: multer.diskStorage({}),
  fileFilter: (req, file, cb) => {
    let ext = path.extname(file.originalname)
    if (ext !== '.jpg' && ext !== '.jpeg' && ext !== '.png') {
      cb(new Error('File type is not supported'), false)
      return
    }
    cb(null, true)
  }
})

const createPost = nextConnect({
  onError(error, req, res) {
    res.status(501).json({ error: error.message })
  }
})

// Adds the middleware to Next-Connect
createPost.use(upload.single('image'))

createPost.post(async (req, res) => {
  const { user } = getSession(req, res)
  // Uploading file to cloudinary
  const result = await cloudinary.uploader.upload(req.file.path)
  // Create a new record with required fields
  const post = {
    image: result.secure_url,
    caption: req.body.caption,
    cloudinaryId: result.public_id,
    userId: user.sub
  }

  // Create a record with the above fields in Airtable
  // the 'create' method accepts and returns an array of records
  const newRecords = await table.create([{ fields: post }])
  res.status(200).json(minifyRecords(newRecords)[0])
})

export default withApiAuthRequired(createPost)

export const config = {
  api: {
    bodyParser: false
  }
}
Enter fullscreen mode Exit fullscreen mode

To access the protected API, you should include the user's context (such as Cookies) along with the request. Otherwise, you will receive the error message not_authenticated. Change the fetch request within getServerSideProps to include the user's session token as a cookie.

/src/pages/index.js

...

export async function getServerSideProps(context) {
  try {
    const res = await fetch('http://localhost:3000/api/posts', {
      headers: { Cookie: context.req.headers.cookie }
    })
    return {
      props: {
        initialPosts: await res.json()
      }
    ...
  }
}
Enter fullscreen mode Exit fullscreen mode

That's all! Now you can see only the posts you have added. Without logging in, you won't be able to access the API routes. Check my app here. If you encounter any problems, try to solve them using this source code. If you prefer downloading, here's the entire codebase on GitHub.

Next Steps

In this tutorial, you have built a full-stack Jamstack app with media management and authentication. You can include some extra features like:

  • Updating and deleting the posts.
  • At present, you can only upload small images. Find a way to upload large images to your application.
  • You can perform media transformations with Cloudinary. Optimize your images before uploading to save space in your cloud.
  • Try to display the user profile and include a route to update or delete the user.
  • Try thinking of some more crazy ways to add additional functionality and share them with the community.

For media management, I prefer Cloudinary. They allow you to store all of your media on a separate cloud other than your database. Auth0 provides an extra layer of protection to your app. You can personalize the login/signup pages to reflect your brand. Airtable is another powerful tool that I like because of its spreadsheet-like user interface. Based on your requirements, you can use any database with Jamstack apps. As previously said, you can use Jamstack to develop full-stack apps by combining various technologies based on your needs.

Content created for the Hackmamba Jamstack Content Hackathon with Auth0 and Cloudinary.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .