Fullstack Guide
Welcome to your next steps with Platformatic applications such as Platformatic Watt, Platformatic DB with SQLite, and the Platformatic Gateway.
In this tutorial, you will build a movie quotes application, where users can add, like and delete quotes from popular movies. This guide will help you setup and run your first full-stack Platformatic application.
While this guide uses SQLite, Platformatic DB also supports PostgreSQL, MySQL, and MariaDB. For more details on database compatibility, see the Platformatic DB documentation.
Prerequisites
Before starting, ensure you have the following installed:
- Node.js (v22.18.0+)
- npm (comes with Node.js)
- A code editor, (e.g., Visual Studio Code)
Create a Watt Project with a Platformatic DB application
To start the Watt creator wizard, run the appropriate command for your package manager in your terminal:
- npm
- yarn
- pnpm
npm create wattpm
yarn create wattpm
pnpm create wattpm
This interactive command-line tool will guide you through setting up a new Watt project. For this guide, please choose the following options:
Hello YOURNAME, welcome to Watt 3.0.0!
? Where would you like to create your project? .
✔ Installing @platformatic/runtime@^3.0.0 using npm ...
? Which kind of application do you want to create? @platformatic/db
✔ Installing @platformatic/db@^3.0.0 using npm ...
? What is the name of the application? db
? What is the connection string? sqlite://./db.sqlite
? Do you want to create default migrations? yes
? Do you want to use TypeScript? no
? Do you want to create another application? no
? What port do you want to use? 3042
After completing the wizard, your Watt application will be ready in the specified folder. This includes example migration files, plugin scripts, routes, and tests within your application directory.
If the wizard does not handle dependency installation, ensure to run npm/yarn/pnpm
install command manually.
wattpm
sets up workspaces for the selected package manager. Running a manual installation with a different package manager may cause issues or trigger warnings.
Start Your API Server
Run the following command in your project directory to start your API server:
- npm
- yarn
- pnpm
npm run dev
yarn run dev
pnpm run dev
Your API server is now live! 🌟 It will automatically serve REST and GraphQL interfaces for your SQL database.
Create a Database Schema
Navigate to the migrations
directory within the web/db
folder of your project directory. This folder contains your database migration files:
001.do.sql
: contains the SQL statements for creating database objects.001.undo.sql
: contains the SQL statements to remove database objects.
For the quote application, you will need a schema configuration for the quotes table and likes. Add the schema configuration below in the 001.do.sql
file to do this:
CREATE TABLE quotes (
id INTEGER PRIMARY KEY,
quote TEXT NOT NULL,
said_by VARCHAR(255) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
This SQL query creates a database table called "quotes" that stores: a unique ID number, the quote's text, who said it, and when it was added to the database.
Create a new file 002.do.sql
in the same folder directory and add a schema below:
ALTER TABLE quotes ADD COLUMN likes INTEGER default 0;
This schema adds the likes column.
Finally, execute your migrations by running:
wattpm db:migrations:apply
Create a Like Quotes Plugin
You will add a Fastify plugin for adding likes to a quote. Create a new file like-quote.js
in the plugins directory of your DB applications.
export default async function plugin (app) {
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 *
`)
return result
}
const schema = {
params: {
type: 'object',
properties: {
id: app.getSchema('Quote').properties.id
},
required: ['id'],
additionalProperties: false
}
}
// Check if the route already exists
if (!app.hasRoute({ method: 'POST', url: '/quotes/:id/like' })) {
app.post('/quotes/:id/like', { schema }, async function (request) {
return { likes: await incrementQuoteLikes(request.params.id) }
})
}
}
Here, you've created a API endpoint that lets users like a quote in the database. incrementQuoteLikes
function updates the database by adding 1 to the quote
's likes
count. The schema checks that the quote ID
is valid.
Apply Schema Migrations
Run the command below for database migrations
npx wattpm db:migrations:apply
Add a Gateway application
Platformatic Gateway integrates different microservices into a single API for more efficient management. For the movie quotes application, you will use the Platformatic gateway to aggregate the DB application, and your frontend application.
Let's create a new Platformatic Gateway
npm create wattpm
This will output:
Hello YOURNAME, welcome to Watt 3.0.0!
Using existing configuration ...
? Which kind of application do you want to create? @platformatic/gateway
? What is the name of the application? gateway
? Do you want to use TypeScript? no
? Do you want to create another application? no
? Which application should be exposed? gateway
Add applications to gateway
In your web/gateway
directory, select the platformatic.json
file and add the DB application to your gateway:
{
"$schema": "https://schemas.platformatic.dev/@platformatic/gateway/3.0.0.json",
"gateway": {
"applications": [
{
"id": "db",
"openapi": {
"url": "/documentation/json"
}
}
],
"refreshTimeout": 1000
},
"watch": true
}
Add a React frontend for Movie Quotes App
Next steps is to add a React (vite) frontend for the movie quotes app. Run the command to create a React.js application:
npm create vite@latest -- web/frontend --template react
Now run wattpm-utils
to import the frontend into Watt. The command will output:
[15:19:48.110] INFO (54409): Application frontend is using Vite. Adding @platformatic/vite to its package.json dependencies.
[15:19:48.132] INFO (54409): Installing dependencies for the project using npm ...
[15:19:56.608] INFO (54409): Installing dependencies for the application db using npm ...
[15:20:00.643] INFO (54409): Installing dependencies for the application frontend using npm ...
[15:20:01.257] INFO (54409): Installing dependencies for the application gateway using npm ...
Setting Up the Frontend Client
To kickstart the project, in your web/frontend/src
directory, run the command to create a Massimo client for your remote server:
npx massimo-cli --frontend http://0.0.0.0:3042 --name client -f web/frontend/src/client
This command will generate a Platformatic frontend client in the specified web/frontend/src folder, which allows a more efficient communication between your frontend and Platformatic DB and gateway application.
Installed Required Packages
To style the application, install the following CSS packages:
npm install tailwindcss postcss autoprefixer
Set up Tailwind CSS by creating the necessary configuration files:
npm create tailwindcss
Ensure your tailwind.config.js
points to the correct paths for your components:
export default {
content: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'],
theme: {
extend: {}
},
plugins: []
}
Building the QuoteList
Component
The core of your frontend lies in the src/components/QuoteList.jsx
file. Here's how it works:
- Data Management:
- Fetches quotes from the API.
- Handles creating new quotes, liking existing ones, and deleting quotes.
- State Handling:
- Manages loading, error states, and real-time updates for user interactions.
Here’s the complete QuoteList
component:
import { useState, useEffect } from 'react'
import { setBaseUrl, dbGetQuotes, dbCreateQuote, dbDeleteQuotes, postQuotesIdLike } from '../client/client.mjs'
// Set the base URL for the API client
setBaseUrl(window.location.origin) // Or your specific API base URL
export default function QuoteList () {
const [quotes, setQuotes] = useState([])
const [error, setError] = useState(null)
const [newQuote, setNewQuote] = useState({ quote: '', saidBy: '' })
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
getQuotes()
}, [])
const getQuotes = async () => {
try {
setIsLoading(true)
const fetchedQuotes = await dbGetQuotes({})
setQuotes(fetchedQuotes)
setError(null)
} catch (error) {
console.error('Error fetching quotes:', error)
setError('Failed to fetch quotes')
} finally {
setIsLoading(false)
}
}
const handleLike = async id => {
try {
await postQuotesIdLike({ id })
getQuotes()
} catch (error) {
console.error('Error liking quote:', error)
setError('Failed to like quote')
}
}
const handleCreate = async e => {
e.preventDefault()
if (!newQuote.quote || !newQuote.saidBy) {
setError('Please fill in both quote and author')
return
}
try {
await dbCreateQuote(newQuote)
setNewQuote({ quote: '', saidBy: '' })
setError(null)
getQuotes()
} catch (error) {
console.error('Error creating quote:', error)
setError('Failed to create quote')
}
}
const handleDelete = async id => {
try {
await dbDeleteQuotes({ id })
getQuotes()
} catch (error) {
console.error('Error deleting quote:', error)
setError('Failed to delete quote')
}
}
return (
<div className="min-h-screen flex flex-col items-center justify-center bg-gray-100 py-8">
<div className="bg-white shadow-md rounded-lg p-8 max-w-lg w-full">
<h1 className="text-3xl font-bold mb-8 text-center text-gray-800">Movie Quotes</h1>
{error && <div className="mb-4 p-4 bg-red-100 border border-red-400 text-red-700 rounded-lg">{error}</div>}
<form onSubmit={handleCreate} className="mb-8">
<div className="flex flex-col sm:flex-row gap-4 mb-4">
<input
type="text"
placeholder="Enter Quote"
value={newQuote.quote}
onChange={e => setNewQuote({ ...newQuote, quote: e.target.value })}
className="w-full p-3 border border-gray-300 rounded-lg shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
<input
type="text"
placeholder="Said By"
value={newQuote.saidBy}
onChange={e => setNewQuote({ ...newQuote, saidBy: e.target.value })}
className="w-full p-3 border border-gray-300 rounded-lg shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<button
type="submit"
className="w-full bg-blue-500 text-white py-3 rounded-lg hover:bg-blue-600 transition duration-200 shadow-md"
>
Add Quote
</button>
</form>
{isLoading ? (
<p className="text-center text-gray-500 text-lg">Loading quotes...</p>
) : quotes.length === 0 ? (
<p className="text-center text-gray-500 text-lg">No quotes available</p>
) : (
<div className="space-y-4">
{quotes.map(quote => (
<div key={quote.id} className="bg-gray-50 p-4 rounded-lg shadow-sm border border-gray-200">
<p className="text-xl font-semibold text-gray-800 mb-1">"{quote.quote}"</p>
<p className="text-md text-gray-600 mb-3">- {quote.saidBy}</p>
<div className="flex justify-between items-center">
<button
onClick={() => handleLike(quote.id)}
className="flex items-center space-x-2 text-blue-500 hover:text-blue-600"
>
<span className="text-2xl">❤️</span>
<span className="text-xl">{quote.likes || 0}</span>
</button>
<button
onClick={() => handleDelete(quote.id)}
className="text-red-500 hover:text-red-600 px-3 py-1 border border-red-500 rounded-lg hover:bg-red-50 transition duration-200"
>
Delete
</button>
</div>
</div>
))}
</div>
)}
</div>
</div>
)
}
Integrating the QuoteList
Component
Update your main app file src/App.jsx
to include the QuoteList component:
import QuoteList from './components/QuoteList'
export default function App () {
return (
<div className="App">
<QuoteList />
</div>
)
}
Add frontend to Gateway
In your web/gateway
directory, add the frontend id
to your gateway platformatic.json
file, update it as shown below:
{
"$schema": "https://schemas.platformatic.dev/@platformatic/gateway/3.0.0.json",
"gateway": {
"applications": [
{
"id": "db",
"openapi": {
"url": "/documentation/json"
}
},
{
"id": "frontend"
}
],
"refreshTimeout": 1000
},
"watch": true
}
Start Your API Server
In your project directory, use the Platformatic CLI to start your API server:
npm run dev
This will:
- Automatically map your SQL database and React frontend to REST using the gateway
- Start the Platformatic Watt server.
Your Platformatic application is now up and running! 🌟