Movie Quotes App Tutorial
This tutorial will help you learn how to build a full stack application on top of Platformatic DB. We're going to build an application that allows us to save our favourite movie quotes. We'll also be building in custom API functionality that allows for some neat user interaction on our frontend.
You can find the complete code for the application that we're going to build on GitHub.
We'll be building the frontend of our application with the Astro framework, but the GraphQL API integration steps that we're going to cover can be applied with most frontend frameworks.
What we're going to cover
In this tutorial we'll learn how to:
- Create a Platformatic API
- Apply database migrations
- Create relationships between our API entities
- Populate our database tables
- Build a frontend application that integrates with our GraphQL API
- Extend our API with custom functionality
- Enable CORS on our Platformatic API
Prerequisites
To follow along with this tutorial you'll need to have these things installed:
- Node.js >= v18.8.0 or >= v20.6.0
- npm v7 or later
- A code editor, for example Visual Studio Code
You'll also need to have some experience with JavaScript, and be comfortable with running commands in a terminal.
Build the backend
Create a Platformatic API
First, let's create our project directory:
mkdir -p tutorial-movie-quotes-app/apps/movie-quotes-api/
cd tutorial-movie-quotes-app/apps/movie-quotes-api/
To start the Platformatic creator wizard, run the appropriate command for your package manager in your terminal:
- npm
- yarn
- pnpm
npm create platformatic@latest
yarn create platformatic
pnpm create platformatic@latest
This interactive command-line tool will guide you through setting up a new Platformatic project. For this guide, please choose the following options:
- What kind of project do you want to create? => Application
- Where would you like to create your project? => quick-start
- Which kind of project do you want to create? => @platformatic/db
- What is the name of the service? => (generated-randomly), e.g. legal-soup
- What is the connection string? => sqlite://./db.sqlite
- Do you want to create default migrations? => Yes
- Do you want to create another service? => No
- Do you want to use TypeScript? => No
- What port do you want to use? => 3042
- Do you want to init the git repository? => No
After completing the wizard, your Platformatic application will be ready in the quick-start
folder. This includes example migration files, plugin scripts, routes, and tests within your service directory.
If the wizard does not handle dependency installation, ensure to run npm/yarn/pnpm
install command manually:
Define the database schema
Let's create a new directory to store our migration files:
mkdir migrations
Then we'll create a migration file named 001.do.sql
in the migrations
directory:
CREATE TABLE quotes (
id INTEGER PRIMARY KEY,
quote TEXT NOT NULL,
said_by VARCHAR(255) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
Now let's setup migrations
in our Platformatic configuration
file, platformatic.db.json
:
{
"$schema": "https://platformatic.dev/schemas/v0.23.2/db",
"server": {
"hostname": "{PLT_SERVER_HOSTNAME}",
"port": "{PORT}",
"logger": {
"level": "{PLT_SERVER_LOGGER_LEVEL}"
}
},
"db": {
"connectionString": "{DATABASE_URL}",
"graphql": true,
"openapi": true
},
"plugins": {
"paths": [
"plugin.js"
]
},
"types": {
"autogenerate": true
},
"migrations": {
"dir": "migrations",
"autoApply": true
}
}
Take a look at the Configuration reference to see all the supported configuration settings.
Now we can start the Platformatic DB server:
npm run start
Our Platformatic DB server should start, and we'll see messages like these:
[11:26:48.772] INFO (15235): running 001.do.sql
[11:26:48.864] INFO (15235): server listening
url: "http://127.0.0.1:3042"
Let's open a new terminal and make a request to our server's REST API that creates a new quote:
curl --request POST --header "Content-Type: application/json" \
-d "{ \"quote\": \"Toto, I've got a feeling we're not in Kansas anymore.\", \"saidBy\": \"Dorothy Gale\" }" \
http://localhost:3042/quotes
We should receive a response like this from the API:
{"id":1,"quote":"Toto, I've got a feeling we're not in Kansas anymore.","saidBy":"Dorothy Gale","createdAt":"1684167422600"}
Create an entity relationship
Now let's create a migration file named 002.do.sql
in the migrations
directory:
CREATE TABLE movies (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE
);
ALTER TABLE quotes ADD COLUMN movie_id INTEGER REFERENCES movies(id);
This SQL will create a new movies
database table and also add a movie_id
column to the quotes
table. This will allow us to store movie data in the
movies
table and then reference them by ID in our quotes
table.
Let's stop the Platformatic DB server with Ctrl + C
, and then start it again:
npm run start
The new migration should be automatically applied and we'll see the log message
running 002.do.sql
.
- GraphQL
- Rest API
Our Platformatic DB server also provides a GraphQL API. Let's open up the GraphiQL application in our web browser:
Now let's run this query with GraphiQL to add the movie for the quote that we added earlier:
mutation {
saveMovie(input: { name: "The Wizard of Oz" }) {
id
}
}
We should receive a response like this from the API:
{
"data": {
"saveMovie": {
"id": "1"
}
}
}
Now we can update our quote to reference the movie:
mutation {
saveQuote(input: { id: 1, movieId: 1 }) {
id
quote
saidBy
createdAt
movie {
id
name
}
}
}
We should receive a response like this from the API:
{
"data": {
"saveQuote": {
"id": "1",
"quote": "Toto, I've got a feeling we're not in Kansas anymore.",
"saidBy": "Dorothy Gale",
"movie": {
"id": "1",
"name": "The Wizard of Oz"
}
}
}
}
Our Platformatic DB server has automatically identified the relationship
between our quotes
and movies
database tables. This allows us to make
GraphQL queries that retrieve quotes and their associated movies at the same
time. For example, to retrieve all quotes from our database we can run:
query {
quotes {
id
quote
saidBy
createdAt
movie {
id
name
}
}
}
To view the GraphQL schema that's generated for our API by Platformatic DB, we can run this command in our terminal:
npx platformatic db schema graphql
The GraphQL schema shows all of the queries and mutations that we can run against our GraphQL API, as well as the types of data that it expects as input.
This is for Open APi Platformatic Rest API with Open API.
Populate the database
Our movie quotes database is looking a little empty! We're going to create a "seed" script to populate it with some data.
Let's create a new file named seed.js
and copy and paste in this code:
'use strict'
const quotes = [
{
quote: "Toto, I've got a feeling we're not in Kansas anymore.",
saidBy: 'Dorothy Gale',
movie: 'The Wizard of Oz'
},
{
quote: "You're gonna need a bigger boat.",
saidBy: 'Martin Brody',
movie: 'Jaws'
},
{
quote: 'May the Force be with you.',
saidBy: 'Han Solo',
movie: 'Star Wars'
},
{
quote: 'I have always depended on the kindness of strangers.',
saidBy: 'Blanche DuBois',
movie: 'A Streetcar Named Desire'
}
]
module.exports = async function ({ entities, db, sql }) {
for (const values of quotes) {
const movie = await entities.movie.save({ input: { name: values.movie } })
console.log('Created movie:', movie)
const quote = {
quote: values.quote,
saidBy: values.saidBy,
movieId: movie.id
}
await entities.quote.save({ input: quote })
console.log('Created quote:', quote)
}
}
Take a look at the Seed a Database guide to learn more about how database seeding works with Platformatic DB.
Let's stop our Platformatic DB server running and remove our SQLite database:
rm db.sqlite
Now let's create a fresh SQLite database by running our migrations:
npx platformatic db migrations apply
And then let's populate the quotes
and movies
tables with data using our
seed script:
npx platformatic db seed seed.js
Our database is full of data, but we don't have anywhere to display it. It's time to start building our frontend!
Build a "like" quote feature
We've built all the basic CRUD (Create, Retrieve, Update & Delete) features into our application. Now let's build a feature so that users can interact and "like" their favourite movie quotes.
To build this feature we're going to add custom functionality to our API and then add a new component, along with some client side JavaScript, to our frontend.
Create an API migration
We're now going to work on the code for API, under the apps/movie-quotes-api
directory.
First let's create a migration that adds a likes
column to our quotes
database table. We'll create a new migration file, migrations/003.do.sql
:
ALTER TABLE quotes ADD COLUMN likes INTEGER default 0;
This migration will automatically be applied when we next start our Platformatic API.
Create an API plugin
To add custom functionality to our Platformatic API, we need to create a Fastify plugin and update our API configuration to use it.
Let's create a new file, plugin.js
, and inside it we'll add the skeleton
structure for our plugin:
// plugin.js
'use strict'
module.exports = async function plugin (app) {
app.log.info('plugin loaded')
}
Now let's register our plugin in our API configuration file, platformatic.db.json
:
{
...
"migrations": {
"dir": "./migrations"
},
"plugins": {
"paths": ["./plugin.js"]
}
}
And then we'll start up our Platformatic API:
npm run dev
We should see log messages that tell us that our new migration has been applied and our plugin has been loaded:
[10:09:20.052] INFO (146270): running 003.do.sql
[10:09:20.129] INFO (146270): plugin loaded
[10:09:20.209] INFO (146270): server listening
url: "http://127.0.0.1:3042"
Now it's time to start adding some custom functionality inside our plugin.
Add a REST API route
We're going to add a REST route to our API that increments the count of
likes for a specific quote: /quotes/:id/like
First let's add fluent-json-schema as a dependency for our API:
npm install fluent-json-schema
We'll use fluent-json-schema
to help us generate a JSON Schema. We can then
use this schema to validate the request path parameters for our route (id
).
You can use fastify-type-provider-typebox or typebox if you want to convert your JSON Schema into a Typescript type. See this GitHub thread to have a better overview about it. Look at the example below to have a better overview.
Here you can see in practice of to leverage typebox
combined with fastify-type-provider-typebox
:
import { FastifyInstance } from "fastify";
import { Static, Type } from "@sinclair/typebox";
import { TypeBoxTypeProvider } from "@fastify/type-provider-typebox";
/**
* Creation of the JSON schema needed to validate the params passed to the route
*/
const schemaParams = Type.Object({
num1: Type.Number(),
num2: Type.Number(),
});
/**
* We convert the JSON schema to the TypeScript type, in this case:
* {
num1: number;
num2: number;
}
*/
type Params = Static<typeof schemaParams>;
/**
* Here we can pass the type previously created to our syncronous unit function
*/
const multiplication = ({ num1, num2 }: Params) => num1 * num2;
export default async function (app: FastifyInstance) {
app.withTypeProvider<TypeBoxTypeProvider>().get(
"/multiplication/:num1/:num2",
{ schema: { params: schemaParams } },
/**
* Since we leverage `withTypeProvider<TypeBoxTypeProvider>()`,
* we no longer need to explicitly define the `params`.
* The will be automatically inferred as:
* {
num1: number;
num2: number;
}
*/
({ params }) => multiplication(params)
);
}
Now let's add our REST API route in plugin.js
:
'use strict'
const S = require('fluent-json-schema')
module.exports = async function plugin (app) {
app.log.info('plugin loaded')
// This JSON Schema will validate the request path parameters.
// It reuses part of the schema that Platormatic DB has
// automatically generated for our Quote entity.
const schema = {
params: S.object().prop('id', app.getSchema('Quote').properties.id)
}
app.post('/quotes/:id/like', { schema }, async function (request, response) {
return {}
})
}
We can now make a POST
request to our new API route:
curl --request POST http://localhost:3042/quotes/1/like
Learn more about how validation works in the Fastify validation documentation.
Our API route is currently returning an empty object ({}
). Let's wire things
up so that it increments the number of likes for the quote with the specified ID.
To do this we'll add a new function inside of our plugin:
module.exports = async function plugin (app) {
app.log.info('plugin loaded')
async function incrementQuoteLikes (id) {
const { db, sql } = app.platformatic
const result = await db.query(sql`
UPDATE quotes SET likes = likes + 1 WHERE id=${id} RETURNING likes
`)
return result[0]?.likes
}
// ...
}
And then we'll call that function in our route handler function:
app.post('/quotes/:id/like', { schema }, async function (request, response) {
return { likes: await incrementQuoteLikes(request.params.id) }
})
Now when we make a POST
request to our API route:
curl --request POST http://localhost:3042/quotes/1/like
We should see that the likes
value for the quote is incremented every time
we make a request to the route.
{"likes":1}
Add a GraphQL API mutation
We can add a likeQuote
mutation to our GraphQL API by reusing the
incrementQuoteLikes
function that we just created.
Let's add this code at the end of our plugin, inside plugin.js
:
module.exports = async function plugin (app) {
// ...
app.graphql.extendSchema(`
extend type Mutation {
likeQuote(id: ID!): Int
}
`)
app.graphql.defineResolvers({
Mutation: {
likeQuote: async (_, { id }) => await incrementQuoteLikes(id)
}
})
}
The code we've just added extends our API's GraphQL schema and defines
a corresponding resolver for the likeQuote
mutation.
We can now load up GraphiQL in our web browser and try out our new likeQuote
mutation with this GraphQL query:
mutation {
likeQuote(id: 1)
}
Learn more about how to extend the GraphQL schema and define resolvers in the Mercurius API documentation.
Enable CORS on the API
When we build "like" functionality into our frontend, we'll be making a client side HTTP request to our GraphQL API. Our backend API and our frontend are running on different origins, so we need to configure our API to allow requests from the frontend. This is known as Cross-Origin Resource Sharing (CORS).
To enable CORS on our API, let's open up our API's .env
file and add in
a new setting:
PLT_SERVER_CORS_ORIGIN=http://localhost:3000
The value of PLT_SERVER_CORS_ORIGIN
is our frontend application's origin.
Now we can add a cors
configuration object in our API's configuration file,
platformatic.db.json
:
{
"server": {
"logger": {
"level": "{PLT_SERVER_LOGGER_LEVEL}"
},
"hostname": "{PLT_SERVER_HOSTNAME}",
"port": "{PORT}",
"cors": {
"origin": "{PLT_SERVER_CORS_ORIGIN}"
}
},
...
}
The HTTP responses from all endpoints on our API will now include the header:
access-control-allow-origin: http://localhost:3000
This will allow JavaScript running on web pages under the http://localhost:3000
origin to make requests to our API.
Wrapping up
And we're done — you now have the knowledge you need to build a complete application on top of Platformatic DB.
We can't wait to see what you'll build next!