Developer Quickstart

In this guide you'll learn how to build a full stack web application with Lens Protocol, Next.js, and Tailwind CSS.

Basic concepts

Lens Protocol allows developers to quickly and easily build web3 social applications.

As a developer you can either use the Lens deployment on Polygon or deploy the protocol yourself to any EVM compatible blockchain network.

In this tutorial you'll use the Lens API to fetch and render a social media feed, navigate to view an individual profile, and fetch and view the user's publications.

The Lens API can be tested at any time here using any of the GraphQL queries in the API docs.

Prerequisites

To complete this tutorial successfully, you must have Node.js installed on your machine.

Getting started

To get started we'll first create and configure the Next.js application.

To generate a new Next.js app, run the following command from your terminal:

npx create-next-app lens-app

Next, change into the new directory and install these dependencies for the GraphQL client:

npm install @apollo/client graphql ethers

Next, we'll install and configure TailwindCSS for styling:

npm install -D tailwindcss postcss autoprefixer

npx tailwindcss init -p

Once Tailwind is installed and configured, open tailwind.config.js and configure the content array to include the paths to the template files in our app:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./pages/**/*.{js,ts,jsx,tsx}",
    "./components/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

Finally, open styles/globals.css and replace the contents of that file with these three lines of code:

@tailwind base;
@tailwind components;
@tailwind utilities;

Creating the API

Creating a basic GraphQL API is simple, we can do it in just a couple of lines of code. We'll also define the first GraphQL query we'll be using in our app.

Create a new file named api.js in the root of the new project and add the following code:

import { ApolloClient, InMemoryCache, gql } from '@apollo/client'

const API_URL = 'https://api.lens.dev'

/* create the API client */
export const client = new ApolloClient({
  uri: API_URL,
  cache: new InMemoryCache()
})

/* define a GraphQL query  */
export const exploreProfiles = gql`
query ExploreProfiles {
  exploreProfiles(request: { sortCriteria: MOST_FOLLOWERS }) {
    items {
      id
      name
      bio
      handle
      picture {
        ... on MediaSet {
          original {
            url
          }
        }
      }
      stats {
        totalFollowers
      }
    }
  }
}
`

The query we've defined here will fetch a list of profiles from Lens based on the profile sort criteria pass in, in our case MOST_FOLLOWERS. You can view all of the sort criteria here, and experiment with other profile queries here.

Fetching and rendering profiles

We can now fetch data using the new API with a single line of code:

const response = await client.query({ query: exploreProfiles })

With that in mind, we'd like to fetch the profiles using this API, then render and style them in our app.

To do so, let's update pages/index.js in our app with the following code:

/* pages/index.js */
import { useEffect, useState } from 'react'
import { client, exploreProfiles } from '../api'
import Link from 'next/link'

export default function Home() {
  /* create initial state to hold array of profiles */
  const [profiles, setProfiles] = useState([])
  useEffect(() => {
    fetchProfiles()
  }, [])
  async function fetchProfiles() {
    try {
      /* fetch profiles from Lens API */
      let response = await client.query({ query: exploreProfiles })
      /* loop over profiles, create properly formatted ipfs image links */
      let profileData = await Promise.all(response.data.exploreProfiles.items.map(async profileInfo => {
        let profile = { ...profileInfo }
        let picture = profile.picture
        if (picture && picture.original && picture.original.url) {
          if (picture.original.url.startsWith('ipfs://')) {
            let result = picture.original.url.substring(7, picture.original.url.length)
            profile.avatarUrl = `http://lens.infura-ipfs.io/ipfs/${result}`
          } else {
            profile.avatarUrl = picture.original.url
          }
        }
        return profile
      }))

      /* update the local state with the profiles array */
      setProfiles(profileData)
    } catch (err) {
      console.log({ err })
    }
  }
  return (
    <div className='pt-20'>
      <div className='flex flex-col justify-center items-center'>
        <h1 className='text-5xl mb-6 font-bold'>Hello Lens 🌿</h1>
        {
          profiles.map(profile => (
            <div key={profile.id} className='w-2/3 shadow-md p-6 rounded-lg mb-8 flex flex-col items-center'>
              <img className='w-48' src={profile.avatarUrl || 'https://picsum.photos/200'} />
              <p className='text-xl text-center mt-6'>{profile.name}</p>
              <p className='text-base text-gray-400  text-center mt-2'>{profile.bio}</p>
              <Link href={`/profile/${profile.handle}`}>
                <p className='cursor-pointer text-violet-600 text-lg font-medium text-center mt-2 mb-2'>{profile.handle}</p>
              </Link>
              <p className='text-pink-600 text-sm font-medium text-center'>{profile.stats.totalFollowers} followers</p>
            </div>
          ))
        }
      </div>
    </div>
  )
}

Next, run the app:

npm run dev

When the app loads, you should see a list of Lens profiles with their image, name, bio, and handle.

🎉 Congratulations! You've built your first Lens application!

Next, we'd like to be able to click on a user's handle and view more details about the user, like their latest posts.

Viewing an individual profile

For the individual profile view we'll need two new API calls:

  1. To fetch the profile details for the individual user
  2. To fetch the publications for the individual user

To enable this, open api.js and add the following two queries:

export const getProfile = gql`
query Profile($handle: Handle!) {
  profile(request: { handle: $handle }) {
    id
    name
    bio
    picture {
      ... on MediaSet {
        original {
          url
        }
      }
    }
    handle
  }
}
`

export const getPublications = gql`
  query Publications($id: ProfileId!, $limit: LimitScalar) {
    publications(request: {
      profileId: $id,
      publicationTypes: [POST],
      limit: $limit
    }) {
      items {
        __typename 
        ... on Post {
          ...PostFields
        }
      }
    }
  }
  fragment PostFields on Post {
    id
    metadata {
      ...MetadataOutputFields
    }
  }
  fragment MetadataOutputFields on MetadataOutput {
    content
  }
`

In pages/index.js you'll notice that we're linking to the user's handle in the html:

<Link href={`/profile/${profile.handle}`}>
    <p class='cursor-pointer text-violet-600    text-lg font-medium text-center mt-2 mb-2'>{profile.handle}</p>
</Link>

Right now, the /profile/${profile.handle} route does not yet exist, so let's create it.

To do so, create a new folder in the pages directory named profile. In it, create a new file named [handle].js.

In this file add the following code:

/* pages/profile/[handle].js */
import { useState, useEffect } from 'react'
import { useRouter } from 'next/router'
import { client, getPublications, getProfile } from '../../api'

export default function Profile() {
  /* create initial state to hold user profile and array of publications */
  const [profile, setProfile] = useState()
  const [publications, setPublications] = useState([])
  /* using the router we can get the lens handle from the route param */
  const router = useRouter()
  const { handle } = router.query

  useEffect(() => {
    if (handle) {
      fetchProfile()
    }
  }, [handle])

  async function fetchProfile() {
    try {
      /* fetch the user profile using their handle */
      const returnedProfile = await client.query({
        query: getProfile,
        variables: { handle }
      })
      const profileData = { ...returnedProfile.data.profile }
      /* format their picture if it is not in the right format */
      const picture = profileData.picture
      if (picture && picture.original && picture.original.url) {
        if (picture.original.url.startsWith('ipfs://')) {
          let result = picture.original.url.substring(7, picture.original.url.length)
          profileData.avatarUrl = `http://lens.infura-ipfs.io/ipfs/${result}`
        } else {
          profileData.avatarUrl = profileData.picture.original.url
        }
      }
      setProfile(profileData)
            /* fetch the user's publications from the Lens API and set them in the state */
      const pubs = await client.query({
        query: getPublications,
        variables: {
            id: profileData.id, limit: 50
        }
      })
      setPublications(pubs.data.publications.items)
    } catch (err) {
      console.log('error fetching profile...', err)
    }
  }

  if (!profile) return null

  return (
    <div className='pt-20'>
      <div className='flex flex-col justify-center items-center'>
        <img
          className='w-64 rounded-full'
          src={profile.avatarUrl}
        />
        <p className='text-4xl mt-8 mb-8'>{profile.handle}</p>
        <p className='text-center text-xl font-bold mt-2 mb-2 w-1/2'>{profile.bio}</p>
        {
            publications.map(pub => (
              <div key={pub.id} className='shadow p-10 rounded mb-8 w-2/3'>
                <p>{pub.metadata.content}</p>
              </div>
            ))
        }
      </div>
    </div>
  )
}

Now, run the app:

npm run dev

When the app loads, you should be able to click on a user's handle and navigate to their profile. In this view, you'll see the user's profile along with a feed of their latest posts!

User profile feedUser profile feed

Next steps

Now that you've built your first basic application, it's time to explore more of the Lens API!

Consider diving into authentication, modules, or learning about gasless transactions and the dispatcher.

Also consider adding the following features to your new app:

  1. Following a user
  2. Searching for users
  3. Creating a post