Build a full-stack Jamstack Application
Tags: jamstack, javascript, webdev, react
Published: 2021-10-16 04:32:59

Many modern approaches for designing flexible and scalable web applications have become popular as browsers have evolved. [Jamstack](https://jamstack.org/) 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](https://res.cloudinary.com/scrapbook/image/upload/v1632641696/scrapbook-preview_okcvwd.png)

[Final demo](https://8xeih.sse.codesandbox.io/) | [Source code](https://codesandbox.io/s/scrapbook-8xeih)

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**](https://nextjs.org/) 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**](https://chakra-ui.com/) provides a plethora of stylish and accessible react components for styling your web application.
- You'll be using [**Airtable**](https://airtable.com/) as a database solution for this application. Airtable is a spreadsheet/database hybrid with a fantastic API for integrating it into your application.
- [**Cloudinary**](https://cloudinary.com/) is a cloud media management platform where you'll upload photos of your scrapbook.
- [**Auth0**](https://auth0.com/) 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](https://codesandbox.io/s/next-chakra-ui-gqtsp) and get ready for coding the scrapbook application. If you prefer to use local development, you should have [Node](https://nodejs.org/) and [NPM](https://www.npmjs.com/) 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>
```

Now, head over to [Airtable](https://airtable.com) 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](https://res.cloudinary.com/scrapbook/image/upload/v1632643426/airtable-base_kt2vr0.png)

Next, navigate to [Airtable API](https://www.airtable.com/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](https://airtable.com/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](https://res.cloudinary.com/scrapbook/image/upload/v1632643535/airtable-api-page_yfgp0s.jpg)

`/.env`

```
AIRTABLE_API_KEY=<your-airtable-api-key>
AIRTABLE_BASE_ID=<your-airtable-base-id>
AIRTABLE_TABLE_NAME=<your-airtable-table-name>
```

Install the [Airtable JavaScript SDK](https://github.com/Airtable/airtable.js) to work with Airtable inside a Next.js application.

```
npm install airtable
```

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

```
npm install @auth0/nextjs-auth0 multer next-connect cloudinary
```

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`

```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 }
```

## Integrate Airtable with Next.js Serverless Functions

Using [API routes](https://nextjs.org/docs/api-routes/introduction), 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](https://nextjs.org/docs/api-routes/introduction). Let's create an API endpoint to retrieve Airtable records.

`/src/pages/api/getPosts.js`

```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! 😕' })
  }
}
```

## 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](https://res.cloudinary.com/scrapbook/image/upload/v1632644798/screely-1632367022550_jxyjdu.png)


`/.env`

```
...
CLOUDINARY_CLOUD_NAME=<your-cloudinary-cloud-name>
CLOUDINARY_API_KEY=<your-cloudinary-api-key>
CLOUDINARY_API_SECRET=<your-cloudinary-api-secret>
```

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

`/utils/cloudinary.js`

```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
```

## 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](https://flaviocopes.com/nextjs-upload-files/)). Create a new file, `createPost.js` inside `/api` for uploading images.

`/src/pages/api/createPost.js`

```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
  }
}
```

Try using [Postman](https://postman.com/) 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](https://nextjs.org/docs/basic-features/data-fetching).

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

`/src/pages/index.js`

```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 😕'
      }
    }
  }
}
```

`/src/components/Card.js`

```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
```

## 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`

```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 }
```

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

`/src/pages/_app.js`

```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
```

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`

```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])

  ...
}
```

## 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](https://betterprogramming.pub/handling-file-inputs-with-javascript-9f2d3a007f05). 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`

```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>
    </>
  )
}
```

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`

```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
```

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](https://auth0.com/) user [**login**](https://auth0.com/auth/login/) to your account or [c](https://auth0.com/signup/)[reate](https://auth0.com/signup/) a free one today. Create a new **Regular web application**. You'll use the [auth0-next.js sdk](https://www.npmjs.com/package/@auth0/nextjs-auth0) 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](https://res.cloudinary.com/scrapbook/image/upload/v1634019680/auth0-settings_fmeur9.png)

`/.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>
```

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.

- **Allowed Callback URLs:** http://localhost:3000/api/auth/callback​, <YOUR_BASE_URL>/api/auth/callback
- **Allowed Logout URLs:** http://localhost:3000, <YOUR_BASE_URL>

Create a new file `auth/[...auth0].js` inside the `/pages/api` directory. Any route you add inside brackets(`[]`) will be treated as [dynamic route](https://nextjs.org/docs/routing/dynamic-routes).

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

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

export default handleAuth()
```

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`

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

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

export default MyApp
```

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`

```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) { ... }
```

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`

```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
```

## 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`

```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! 😕' })
  }
})
```

`/pages/api/createPost.js`

```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
  }
}
```

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`

```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()
      }
    ...
  }
}
```

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](https://8xeih.sse.codesandbox.io/). If you encounter any problems, try to solve them using this [source code](https://codesandbox.io/s/scrapbook-8xeih). If you prefer downloading, here's the entire [codebase on GitHub](https://github.com/giridhar7632/scrapbook-demo).

## 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](https://content.hackmamba.io/) Jamstack Content Hackathon with Auth0 and Cloudinary.