Building an App on Airtable using Prisma, Next.js, and Sequin
Let’s build a website that shows off what you can build on your Airtable data using Prisma, Next.js, and Sequin. The website you’ll build will allow users to explore an Airtable base with 10,000 records about tech startup acquisitions. When completed, users will be able to filter by acquisition price so they can see which startups trotted into a new company as a unicorn and which got aqua-hired: You will be using Next.js and Prisma to build your app. Next.js is a React framework that allows you to build a production-level app in an easy way. It’s the fastest way to get started with React. Prisma is a next-generation ORM for Node.js and TypeScript. It helps you to access your Sequin database in a type-safe manner so you make fewer errors and build your applications faster than ever. Prisma and Next.js combined with Airtable via Sequin is a powerful combination. Let’s get started.Airtable Setup
For this tutorial, you will be using the Startup Acquisitions Airtable template. This simple base contains one table,Acquisitions, with 7 fields:
To add this template to your workspace, click the Copy base button in the top right corner:
Then, select your Airtable workspace in the modal that appears:
This will duplicate the entire base into your workspace so you can edit the base and access it through the API key.
Sequin Setup
You have setup your Airtable base. Now set up Sequin to replicate your Airtable base to a Postgres database. Go to https://console.sequin.io/signup and create an account. Connect your Airtable base by going through the tutorial or clicking the Add Base button. You’ll be prompted to enter your Airtable API key. After that, select the Startup Acquisitions base and all its tables. Then click Start Syncing. Sequin will immediately provision you a Postgres database and begin syncing all the data in your Airtable base to it. You’ll be provided with credentials for you new database. Keep these handy as you’ll use them to connect your Sequin database to your app using Prisma. Don’t worry if you lose the credentials. You can always access them by clicking on Connect. You now have access to a fully hosted Postgres database that is in sync with the Airtable base.Why use Prisma
Prisma simplifies database access in the application and removes the complexity of writing queries. Currently, it only supports mySQL, SQLite, and (lucky for you) PostgreSQL. The Prisma client provides auto-generated, type-safe database access. It has a simple and powerful API for working with relational data and transactions. And as a cherry on top, it allows visual data management with Prisma Studio. Prisma and Sequin are the perfect combination to query the Airtable base faster without having to write any PostgresSQL queries.Prerequisites
In this tutorial, you will be using Next.js, Prisma, urql and Tailwind CSS (for styling). To help you get right into the project, I have created a starter repo. Clone the repo and let’s get started!Folder structure
First, take a look at the folder structure:client/ folder is bootstrapped from create-next-app while the server/ folder was generated manually to house Prisma.
Go inside of the startup-acquisitions/ folder and cd into it as follows:
Prisma) to query all your Airtable data via Sequin and GraphQL.
Backend
Navigate into theserver/ directory:
Setting up Prisma
Install the following dependencies:prismais a Prisma CLI which is used to generate a new Prisma project, introspect an existing database, generate artifacts (i.e, Prisma Client) and much more.- You’ll use
@prisma/clientas an auto-generated query builder that enables type-safe database access and reduces boilerplate. - You’ll also use
apollo-serverwhich is a open-source GraphQL server that works with pretty much all Node.js HTTP server frameworks. graphql-scalarsprovide access to custom GraphQL scalars that are common but not supported by the GraphQL specification yet.nexusallows you to strongly type your GraphQL schema in a code-first declarative manner.nexus-plugin-prismais the glue that makesnexuswork withprisma.
prisma which contains a file called schema.prisma and a .env file in the root of the project. schema.prisma contains the Prisma schema with your database connection and the Prisma Client generator. .env is a dotenv file for defining environment variables (used for your database connection).
Connect your database
To connect your Sequin database to Prisma, you need to set theurl field of the datasource block in your Prisma schema to your Sequin database connection URL.
Out of the box, the prisma/schema.prisma file looks like this:
url is set via an environment variable which is defined in .env.
Go ahead and edit the .env file so that the DATABASE_URL now points to your Sequin database.
To do so, you can simply copy and past your database url from the Sequin console - just click the black Connect button on your resource and then find the connection URL:
Your .env file will look something like this:
DATABASE_URL, save the .env file.
Introspect the database
Now let’s introspect the database by using theprisma introspect command. This command will automatically generates a database schema from your Sequin database inside the prisma/schema.prisma file.
prisma/schema.prisma will changed to:
- An
acquisitionsmodel which takes the table by the same name from Airtable. It also maps all the fields and datatypes properly. - A
sync_metamodel which is a special Sequin table that tracks the performance of the Sequin sync.
schema.prisma as you want.
For instance, one edit you need to make here is for price_amount. The price_amount field, which captures the total acquisition price, contains some huge, multi-billion numbers. They cannot fit into Int so you’ll be using BigInt.
So go ahead and change the price_amount field to use BigInt in schema.prisma:
price_amount to type BigInt, save the schema.prisma file and define the BigInt scalar.
First, lets take a quick step back. In GraphQL, there are two different kinds of types.
- Scalar types represent concrete units of data. The GraphQL spec has five predefined scalars:
String,Int,Float,Boolean, andID. Here, you need to create custom scalars likeBigIntandDateTime. - Object types have fields that express the properties of that type and are composable. Here, you’ll need to create an object type for
Acquisition.
./node_modules/@prisma/client.
Now, open up the api/graphql/ folder:
You’ll see several files in this directory that will define several custom datatypes:
BigInt.tsallows you to support large numbers as GraphQL doesn’t support it by default.DateTime.tsallows support forDateTimeas GraphQL doesn’t have support for it by default.Acquisition.tscontains the GraphQL model of your schema. Since you have only one table in your schema, you only need one model. If you had multiple tables, you would need multiple schema files.Query.tscontains all the GraphQL queries needed for fetching data by the client-side of the app.index.tsis just for ease of use. It imports everything insideapi/graphqlfolder and re-exports it like a barrel. It acts as a simple aggregator of the other files to make imports easier down the road.
BigInt.ts
As we’ve noted, GraphQL doesn’t have support forBigInt, so you’ll need to use GraphQLBigInt from graphql-scalars. It allows you to handle large number like billion in your price_amount field. It converts the large values to string since JavaScript cannot handle very large numbers by default.
Declare your custom scalar type BigInt in the BigInt.ts file.
DateTime.ts
DateTime is also not supported by GraphQL so you need to create a custom type which is a nexus wrapper around GraphQLDateTime from graphql-scalars.
Declare a custom scalar type DateTime in the DateTime.ts file.
Acquisition.ts
The most basic components of a GraphQL schema are object types, which represent the kinds of objects you can fetch from your service, and what fields it has. You’ll create yourAcquisition model here. This should be the same as the model that you already generated in schema.prisma after introspecting your SyncInc database with npx prisma introspect.
Acquisition is a GraphQL Object Type, meaning it’s a type with some fields. Most of the types in your schema will be object types. Add the following type definition to the Acquisition.ts file:
objectType from nexus and providing each of the fields defined in your model in schema.prisma with their respective types.
Note: BecauseDateTimeandBigIntare custom scalar types. That’s why you have to access them fromt.fieldusing atypeoption as a 2nd parameter. You must also pass the customtypetomakeSchemaas you have down below inschema.ts.
Define your GraphQL query
Now, you’ll declare the queries you want your GraphQL server to expose to your client inQuery.ts.
Open up the Query.ts file:
Query.ts
As you’ll recall, you want the user to be able to filter the list of startup acquisitions by different price thresholds to see which was a unicorn acquisition and which was a bit of a “soft landing.” To do so, you’ll first get the query to return theAcquisition details between a minimum price and a maximum price.
- Starting off, you import
queryTypefromnexus. This tellsnexusthat you are declaring aQueryand not a custom type. - Next, you name your query
getAcquisitionsByPriceinside thefield. The same name should be used when calling it from the GraphQL playground and the client-side.t.nonNull.list.field()tellsnexusthat the result should be a non-nulllistof values. - The list should be of type
Acquisitionas described by thetypefield inside of it. This is the return type inside of theresolvefolder. - The
argsparameter takes inminPriceandmaxPrice. ThenonNullfunction around them makes these required fields.minPriceandmaxPriceare of custom scalar typesBigIntas you have declared in theBigInt.tsfile. - The arguments you passed into the query in the previous step can be accessed in the 2nd parameter of the
resolvefunction. - You then use
findManywhich returns a list of values of typeAcquisition. You have specified this above when you wrotetype. It is also why you usedt.nonNull.list.field()previously. - The query in
wheremakes sure that theprice_amountis greater than or equal tominPriceandprice_amountis less than or equal tomaxPrice. You use theANDoperator in this case as you want both conditions to betrueat the same time. - Finally, you return the
acquisitionsvariable which is the result ofctx.prisma.acquisitions.findMany().
undisclosed - or in this case a null value. To do so, you need to get the Acquisition details of deals that are undisclosed as well as all the acquisitions whose price amount is known by using a boolean operator:
- The
argsparameter takes inundisclosed.undisclosedis of typeBoolean. It is also a required field. - The arguments you passed into the query can be accessed in the 2nd parameter of the
resolvefunction. - You then declare a variable
priceAmountZerowhich literally lives up to its name. It compares theprice_amountvariable to the numberzero. When theprice_amountequals zero, it means that the startup acquisition numbers are undisclosed. notUndisclosedvariable returns allpriceAmountZerovalues if itstrueor else all values that are not equal to zero when itsfalse. You use theNOToperator to return only the startup acquisitions whoseprice_amountis known.- You spread your
notUndisclosedvariable declared inside thewhereclause.
Acquisition details either descending or ascending. You’ll sort on two parameters: price and startup name.
- The
argsparameter takes insortBy.sortByis of typeString. It is also a required field. - The arguments you passed into the query can be accessed in the 2nd parameter of the
resolvefunction. sortByPricechecks if thesortByvariable includespricewhile thesortByDescchecks if thesortByvariable includesdesc. It returns a boolean value.- You sort it by
pricedescending if it includes bothdescandprice. If it includes onlyprice, then you sort it ascending viaprice_amount. If it includes onlydescbut notprice, then you sort by startup name ,i.e,acquired_startup. If it doesn’t include bothdescandprice, then you sort by startup name ascending. - Put
orderByvariable inside thewhereclause.
skip and take.
- The
argsparameter takes inskipandtake.skipandtakeare of typeInteger. - The arguments you passed into the query can be accessed in the 2nd parameter of the
resolvefunction. - You pass in
skipandtakewhich you received as arguments. It allows you to paginate through the data.skipskips a certain number of results andtakeselects a limited range of results.skipis similar toOFFSETin SQL andtakeis similar toLIMITin SQL.
Query.ts file should look like:
GraphQL API
You have now setup your GraphQL query, your model and the scalar data types needed for the app. Now, lets glue them together to the Prisma Client.context.ts
Go inside theapi/ directory and open up context.ts.
The context.ts file allows you to access the typings of your schema in your IDE. Enter the following code:
PrismaClient and then export the context.
schema.ts
Open upschema.ts and paste the following:
The schema.ts file is responsible for generating nexus-typegen.ts and schema.graphql.
makeSchema method from the nexus package to combine the models and add Acquisition, BigInt, DateTime, and Query to the types array.
You also add nexusPrisma to the plugins array which ensures that nexus and prisma work together nicely.
Open up server.ts and paste the following:
server.ts
Theserver.ts file starts a simple GraphQL server using apollo-server.
ApolloServer takes in schema and context variables defined in the previous files so it knows the schema it should generate on the GraphQL Playground and the typings it should provide in your IDE.
Start your server
Now, run thedev command in the terminal by typing:
http://localhost:4000 and put in the following query:
Frontend
Go inside theclient/ directory from the root of your project.
Define the GraphQL query client-side
In the client-side application, you need to get the startup details by querying the GraphQL endpoint you just created in the server. To do so you’ll be creatinggetAcquisitionsByPrice.js inside your graphql/ folder.
Create the graphql/ folder in order to store your GraphQL queries in one location. In this app, it doesn’t matter much as you only have one GraphQL query but it does make sense as the app grows large and you need multiple GraphQL queries.
Note: In larger apps, you can also dividegraphql/folder into multiple subfolders depending on their type likequeries/,mutations/,fragments/, etc…
getAcquisitionsByPrice.js and paste the following:
Note: TheHere, you’re setting up the front-end to query the GraphQL endpoint you just built:gqlvariable usingString.rawsimply adds syntax highlighting in VSCode. You can remove it if you want. It has no difference on the code whatsoever.
getAcquisitionsByPrice. You’ll see that the query perfectly matches the query you ran in GraphQL Playground while implementing the server side. The only difference is you have used variables instead of values (denoted with $).
The datatypes defined alongside the variables must match to the ones in Prisma. Notice, the ! at the end of each variable. It means that the value is required. If the value is not required, then you can omit the !.
Build the Card component
You’ll be creating a Card component to display a card with startup details. It will contain the startup name, the parent company it got acquired by, the price for which it got acquired and much more. Now, go inside thecomponents/ folder, open up Card.js and paste the following:
- You take two props
startupandindexand render them into cards in yourrender()function. - Recall that in Airtable
price_amountis equal to0for any undisclosed acquisition values. Since you are usingBigIntforprice_amount, the value is first converted intonumberfromstringusingparseInt. - You are then using some gradients based on the
indexprop to create six, nice alternate colors.
Query for your data
Now you’ll useurql as the lightweight GraphQL client. This allows you to communicate from the client to the server and fetch the startup details.
You will query the GraphQL query getAcquisitionsByPrice that is already set up on the backend to get a list of acquisitions.
Open up AcquisitionList.js and paste the following:
AcquisitionList component is responsible for calling the GET_ACQUISITION_BY_PRICE query.
- First, you also store the
skipvalue in a variable so you can skip a bunch of pages using pagination. You then have auseQueryfunction fromurqlwhich takes in a bunch of variables needed for theGET_ACQUISITION_BY_PRICEquery. - You show
loadinganderrormessages when there is nodata. You display the loading and the error message with a little helperTextcomponent.
data is available if someone clicked Load More... button. You will also clear the old data if someone changes the minPrice or the maxPrice.
- First, you use
acqto temporarily store the startup acquisitions data. You don’t directly use thedata.getAcquisitionsByPricereturned byresultsince you need to push the old data (see point no.2) when someone clicks theLoad More...button. - You listen to the changes made to the
datavariable. If newdatacomes in, for example, when someone clicksLoad More...button, then you combine it with the old data ,i.e,acq. - You listen to the changes made to the
minPriceandmaxPricevariable. When someone changes the ranges, you then empty the state by settingacqto[]. This way you only display the new data betweenminPriceandmaxPrice.
Cards.
- First, filter the
startuparray byundisclosedvariable. This variable shows or hides acquisitions whose numbers wereundisclosed. - Sort the result from
filterby eithernameorpriceascending or descending as the user has selected. By default, the sort is bypricedescending. - You again show
loadinganderrorindicators. This is different from the aboveloadinganderrorindicator as it only shows after you havedataand after theLoad More...button is clicked. - Increase the
skipvalue by20so it pulls in 20 more startups when you click on theLoad More...button
AcquisitionList.js file should look like:
Add filtering and sorting
You will use Headless UI to get a simple un-styled API for theSwitch and Toggle component.
Now open up Toggle.js and paste the following:
enabled and setEnabled variables as props and pass them to a Switch component from @headlessui/react. @headlessui/react offers a Toggle component which looks like:
You change a little bit of styling to make it look like:
Now, let’s do the same with sliders. You will use react-slider package for implementing range sliders.
Open up Range.js and paste the following:
price and setPrice as props. price is an array of 2 values: minPrice and maxPrice. Both the input’s are readOnly so you can select them but you cannot edit its values as you only want to edit the values through the range slider.
It should look like:
Now you will use Heroicons to get beautiful hand-crafted open-source SVG icons.
Open up SortBy.js and paste the following:
options, selectedOption and setSelectedOption variables as props. @headlessui/react offers a Select component which looks like:
The SortBy component looks complex but its just a copy-paste from the Headless UI Select docs while tweaking it a little bit (mostly styling) according to your needs.
In your app, it should look like:
Open up Home.js and paste the following:
- You use
priceRangein theReact.useState()hook which ranges fromzeroto100 billion. The first value is forminPriceand the second value is formaxPricein the range slider. You also use temporaryacquisitionPriceRangewhich is similar topriceRange. It is used to set the value 2 seconds later thanpriceRange(see the next point no. 2). - You listen to the changes in
priceinReact.useEffect(). When thepricechanges, you set theacquisitionPriceRangetopricein 2000 ms ,i.e, 2 seconds later. You passpriceto theRangeso thepricevariable gets updated whenever someone changes the range slider. Finally, you passacquisitionPriceRangetoAcquisitionListwhich is a 2-seconds delayedpriceso that you don’t update the list as soon as someone changes the range slider. You wait 2 seconds. - Use
enabledfor yourTogglecomponent which is then passed toAcquisitionListas a switch forundisclosed. - Finally, you have your list of options to display in the
SortBycomponent. You keep track of the selection via theselectedOptionvariable which is also passed toAcquisitionListassortByto sort appropriately.
Conclusion
Using Airtable as your data source and SyncInc to convert Airtable to Postgres database helps you create blazing fast client-facing apps with the query language you already know (SQL). You used Next.js as your React-powered front-end framework and Prisma as your ORM of choice. Prisma makes it simple to query the database by using static typing which allows you to code with confidence. The built-in autocompletion allows you to write applications at lightning speed. You used Tailwind CSS to quickly make the applications look great. Thi focus on writing the logic rather than writing CSS. You also made use of Headless UI to easily create customSwitch and Select component.
You used urql as a lightweight GraphQL client to call your backend.
In conclusion, you launched a fully-functional app using real data from Airtable that is in real-time sync with SyncInc.
