Introduction
Today we are going to learn about the app deployment platform fly.io and the globally distributed S3-compatible object storage service Tigris. Both platforms are deeply connected which makes them a great choice for your projects. You get the app deployment experience from fly.io and the object storage features from Tigris. App deployment is pretty self-explanatory so instead let's first get a quick introduction into bucket storage which Tigris uses.
An Amazon S3 bucket is a resource for public cloud storage that is accessible via the Simple Storage Service (S3) platform of Amazon Web Services (AWS). Low-latency storage is a feature that the globally distributed, S3-compatible object storage service Tigris uses. This means that we can access Amazon's S3 buckets on Tigris for our storage needs. Tigris has also been fully integrated directly with Fly.io and is also completely integrated with flyctl which operates on hardware from Fly.io. Fly.io's command-line interface, flyctl, allows you to deal with the platform from account creation to application deployment.
To learn the fundamentals of these platforms we shall build a user database application. It's pretty straightforward, essentially we can perform full CRUD requests which means being able to read, add, update, and delete our user data. Next.js will be our main framework because it allows us to build full-stack apps without having to create a separate server.
You can learn more about fly.io and tigris, we will need to create an account on both platforms for this project regardless. Anyway with the theory out of the way let's get started in the next section as we create our accounts and start building the app.
For this project you can find the codebase online here https://github.com/andrewbaisden/fly-tigris-user-database.
Creating an account on fly.io and Tigris
Just follow these steps to get up and running on both platforms.
Firstly you need to create an account on fly.io because to utilise Tigris, you'll need a Fly.io account.
Next, install the flyctl command line tool on your computer which is essential for setting up your account to deploy your applications.
Ok, let's move on to the next stage which is where we will set up our project as well as our Tigris bucket storage.
Setting up our user database project
Start by navigating to a directory on your computer where you plan to create the project. Then create a folder called fly-tigris-user-database
and cd
into it. Now run the command to setup a Next.js project inside of that folder:
It's important that for the configuration you select yes for Tailwind CSS and the App router because we will need them in this project.
npx create-next-app .
We just have one package to install and that is @aws-sdk/client-s3
which we need for connecting to our bucket. Install it with this command:
npm install @aws-sdk/client-s3
Ok, good now it is time to create a bucket for the project we just created so refer to their official documentation here https://www.tigrisdata.com/docs/get-started/.
Just run this command to create a bucket:
fly storage create
Now on the setup screen choose a name for your bucket. I think that the name needs to be unique so you can't use a name that someone else has chosen. Alright now for the most important stage, you should have your AWS and bucket secrets like the example here:
AWS_ACCESS_KEY_ID: yourkey
AWS_ENDPOINT_URL_S3: https://fly.storage.tigris.dev
AWS_REGION: auto
AWS_SECRET_ACCESS_KEY: your secret access
BUCKET_NAME: your bucket name
Create a .env.local
file inside the root of your Next.js project and copy and paste all of those secret environment variables. We are not done yet now to get these environment variables to work properly inside our Next.js application will require us to adjust their names by making them public. See the example below and make the change to your .env.local
file. Also at the top create a separate environment variable for localhost we will need this later when accessing our routes locally. When we upload our codebase to fly.io we will change the NEXT_PUBLIC_SECRET_HOST
environment variable to our online route.
NEXT_PUBLIC_SECRET_HOST: http://localhost:3000
NEXT_PUBLIC_SECRET_AWS_ACCESS_KEY_ID: yourkey
NEXT_PUBLIC_SECRET_AWS_ENDPOINT_URL_S3: https://fly.storage.tigris.dev
NEXT_PUBLIC_SECRET_AWS_REGION: auto
NEXT_PUBLIC_SECRET_AWS_SECRET_ACCESS_KEY: your secret access
NEXT_PUBLIC_SECRET_BUCKET_NAME: your bucket name
Right now on the Tigris documentation page if you click on the dashboard button and sign into your account you should see your newly created bucket like in my example shown here:
Great that's the first phase done we have a bucket to store our app data online so we can get started on creating our application now in the next section.
Building our user database application
Ok, this will be split into two sections. The first thing we will do is get our server built and running so that we can test out the CRUD endpoints. Then we will finish off by building our front end.
Creating our user database server
So to begin with let us create our backend architecture. We are going to be creating 4 endpoints so 1 for each CRUD request. We also need a helper file which will have some functions for getting users from our object storage. If you have not already cd
into the root of the project and run the commands below which will set up all of our files and folders quickly:
cd src/app
mkdir api
mkdir api/deleteuser api/getusers api/postuser api/updateuser
touch api/deleteuser/route.js
touch api/getusers/route.js
touch api/postuser/route.js
touch api/updateuser/route.js
mkdir helpers
touch helpers/getUsers.js
Good that was quick now we just have to add the code to our 5 files and our backend API will be ready to test.
Let's do the helpers file first so put this code inside of helpers/getUsers.js
. Like I said earlier this file with have functions for fetching our users, users by email and users by ID:
import {
S3Client,
ListObjectsV2Command,
GetObjectCommand,
} from '@aws-sdk/client-s3';
const streamToString = (stream) =>
new Promise((resolve, reject) => {
const chunks = [];
stream.on('data', (chunk) => chunks.push(chunk));
stream.on('error', reject);
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
});
export async function fetchAllUsersFromS3() {
try {
const s3 = new S3Client({
region: process.env.NEXT_PUBLIC_SECRET_AWS_REGION,
endpoint: process.env.NEXT_PUBLIC_SECRET_AWS_ENDPOINT_URL_S3,
credentials: {
accessKeyId: process.env.NEXT_PUBLIC_SECRET_AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.NEXT_PUBLIC_SECRET_AWS_SECRET_ACCESS_KEY,
},
});
const commandDetails = new ListObjectsV2Command({
Bucket: process.env.NEXT_PUBLIC_SECRET_BUCKET_NAME,
MaxKeys: 10,
});
const { Contents } = await s3.send(commandDetails);
console.log('List Result', Contents);
if (!Contents) {
console.log('no users');
} else {
const users = await Promise.all(
Contents.map(async (item) => {
const getObject = new GetObjectCommand({
Bucket: process.env.NEXT_PUBLIC_SECRET_BUCKET_NAME,
Key: item.Key,
});
const { Body } = await s3.send(getObject);
const data = await streamToString(Body);
const userObject = JSON.parse(data);
console.log('Data', data);
return userObject;
})
);
return users;
}
} catch (e) {
console.error(e);
throw e;
}
}
export async function getUserById(users, userId) {
if (!users) {
console.log('no users');
} else {
return users.find((user) => user.id === userId);
}
}
export async function getUserByIdEmail(users, email) {
if (!users) {
console.log('no users');
} else {
return users.find(
(user) => user.email.toLowerCase() === email.toLowerCase()
);
}
}
Alright, just the routes left now. We will do the GET route which is where we are going to be fetching all of our users from the Tigris bucket, so put this code into getusers/route.js
:
import {
S3Client,
ListObjectsV2Command,
GetObjectCommand,
} from '@aws-sdk/client-s3';
export async function GET() {
const streamToString = (stream) =>
new Promise((resolve, reject) => {
const chunks = [];
stream.on('data', (chunk) => chunks.push(chunk));
stream.on('error', reject);
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
});
try {
const s3 = new S3Client({
region: process.env.NEXT_PUBLIC_SECRET_AWS_REGION,
endpoint: process.env.NEXT_PUBLIC_SECRET_AWS_ENDPOINT_URL_S3,
credentials: {
accessKeyId: process.env.NEXT_PUBLIC_SECRET_AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.NEXT_PUBLIC_SECRET_AWS_SECRET_ACCESS_KEY,
},
});
const listParams = {
Bucket: process.env.NEXT_PUBLIC_SECRET_BUCKET_NAME,
MaxKeys: 10,
};
const list = new ListObjectsV2Command(listParams);
const { Contents } = await s3.send(list);
console.log('List Result', Contents);
if (!Contents || Contents.length === 0) {
console.log('No users found');
return new Response(JSON.stringify({ error: 'No users found' }), {
status: 404,
});
}
const users = await Promise.all(
Contents.map(async (item) => {
const getObjectParams = {
Bucket: process.env.NEXT_PUBLIC_SECRET_BUCKET_NAME,
Key: item.Key,
};
const getObject = new GetObjectCommand(getObjectParams);
const { Body } = await s3.send(getObject);
const data = await streamToString(Body);
console.log('Backend API GET Data:', data);
return JSON.parse(data);
})
);
return new Response(JSON.stringify(users), { status: 200 });
} catch (e) {
console.error('Error:', e);
return new Response(
JSON.stringify({ error: e.message || 'Unknown error' }),
{ status: 500 }
);
}
}
Up next is the POST route this is where we send our data to our Tigris bucket. Put this code in postuser/route.js
:
import { fetchAllUsersFromS3, getUserByIdEmail } from '../../helpers/getUsers';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
export async function POST(req) {
try {
const { firstname, lastname, email, password } = await req.json();
const id = crypto.randomUUID();
const data = { firstname, lastname, email, password, id };
console.log('Request body data', data);
const allUsers = await fetchAllUsersFromS3();
console.log('all users', allUsers);
const existingUser = await getUserByIdEmail(allUsers, email);
console.log(existingUser, email);
if (existingUser) {
return Response.json({
error: 'Email address already in use',
});
}
const s3 = new S3Client({
region: process.env.NEXT_PUBLIC_SECRET_AWS_REGION,
endpoint: process.env.NEXT_PUBLIC_SECRET_AWS_ENDPOINT_URL_S3,
credentials: {
accessKeyId: process.env.NEXT_PUBLIC_SECRET_AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.NEXT_PUBLIC_SECRET_AWS_SECRET_ACCESS_KEY,
},
});
const commandDetails = new PutObjectCommand({
Body: JSON.stringify(data),
Bucket: process.env.NEXT_PUBLIC_SECRET_BUCKET_NAME,
Key: email,
});
await s3.send(commandDetails);
return Response.json({ message: 'User added' });
} catch (e) {
console.error(e);
return Response.json({ error: 'Failed to create user' });
}
}
Follow that up with our UPDATE route, this code lets us update our data inside the bucket. The code goes into updateuser/route.js
:
import { getUserById, fetchAllUsersFromS3 } from '../../helpers/getUsers';
import {
S3Client,
DeleteObjectCommand,
PutObjectCommand,
} from '@aws-sdk/client-s3';
export async function PUT(req) {
try {
const { firstname, lastname, email, originalEmail, id } = await req.json();
console.log('request data', firstname, lastname, email, originalEmail, id);
const allUsers = await fetchAllUsersFromS3();
console.log('all users', allUsers);
const userToUpdate = await getUserById(allUsers, id);
console.log('user to update', userToUpdate);
const user = allUsers.find((user) => user.id === id);
const userEmail = user ? user.email : null;
console.log('User Email', userEmail);
if (!userToUpdate) {
return Response.json({ error: 'User not found' });
}
if (!originalEmail || !email) {
return Response.json({
error: 'Both originalEmail and email are required for update',
});
}
const data = { firstname, lastname, email, id };
console.log('Updated data', data);
const s3 = new S3Client({
region: process.env.NEXT_PUBLIC_SECRET_AWS_REGION,
endpoint: process.env.NEXT_PUBLIC_SECRET_AWS_ENDPOINT_URL_S3,
credentials: {
accessKeyId: process.env.NEXT_PUBLIC_SECRET_AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.NEXT_PUBLIC_SECRET_AWS_SECRET_ACCESS_KEY,
},
});
console.log('Original email', originalEmail);
console.log('New email', email);
if (userEmail === originalEmail) {
console.log('The emails are the same so its a match');
const deleteCommand = new DeleteObjectCommand({
Bucket: process.env.NEXT_PUBLIC_SECRET_BUCKET_NAME,
Key: originalEmail,
});
await s3.send(deleteCommand);
const putCommand = new PutObjectCommand({
Body: JSON.stringify(data),
Bucket: process.env.NEXT_PUBLIC_SECRET_BUCKET_NAME,
Key: email,
});
await s3.send(putCommand);
return Response.json({ message: 'User updated successfully' });
} else {
console.log('Error: The emails do not match');
return Response.json({ error: 'Failed to update user' });
}
} catch (e) {
console.error(e);
}
}
All thats left is our DELETE route which is used for removing data from our bucket. Add this code to deleteuser/route.js
:
import { S3Client, DeleteObjectCommand } from '@aws-sdk/client-s3';
import { fetchAllUsersFromS3, getUserById } from '../../helpers/getUsers';
export async function DELETE(req) {
try {
const id = await req.json();
console.log('Id', id.id);
const allUsers = await fetchAllUsersFromS3();
console.log('all users', allUsers);
const userToDelete = await getUserById(allUsers, id.id);
console.log('user to delete', userToDelete);
if (!userToDelete) {
return Response.json({ error: 'User not found' });
}
const userEmail = userToDelete.email;
const s3 = new S3Client({
region: process.env.NEXT_PUBLIC_SECRET_AWS_REGION,
endpoint: process.env.NEXT_PUBLIC_SECRET_AWS_ENDPOINT_URL_S3,
credentials: {
accessKeyId: process.env.NEXT_PUBLIC_SECRET_AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.NEXT_PUBLIC_SECRET_AWS_SECRET_ACCESS_KEY,
},
});
const deleteCommand = new DeleteObjectCommand({
Bucket: process.env.NEXT_PUBLIC_SECRET_BUCKET_NAME,
Key: userEmail,
});
await s3.send(deleteCommand);
return Response.json({ message: 'User deleted successfully' });
} catch (e) {
console.error(e);
return Response.json({ error: 'Failed to delete user' });
}
}
Ok, good that's it we are done with the backend. Start the server with the usual run code and test out those routes to make sure that you can connect to your bucket and use all of the CRUD requests:
npm run dev
To test the backend use an API testing tool like Postman. Take a look at the example screenshots for reference:
Doing GET Requests
GET requests are pretty easy just go to http://localhost:3000/api/getusers.
Doing POST Requests
POST requests can be done here http://localhost:3000/api/postuser.
Doing PUT Requests
For PUT requests go to this route http://localhost:3000/api/updateuser. It's important to note that you MUST put the original email address for that ID otherwise it's not going to work. And remember this for the front end too because only basic error handling has been implemented.
Doing DELETE Requests
DELETE requests can be done here http://localhost:3000/api/deleteuser.
Great our backend should be fully working now just the frontend left then we can deploy our app online to fly.io.
Creating our user database UI
Now for the front end, we only need to create 4 custom hooks and each one is self-explanatory. These hooks perform our CRUD requests and that's all. Other than that we need to modify a few files so that we get our Tailwind CSS styles working and then we can finish with building our frontend component. In a production-level application it would be wise to create components for all of our UI and logic however for this tutorial we will put all of the main code in one file so we don't have to spend a long time building separate components.
Before we start, run this script from the root project folder we can set the project folder structure for our custom hooks:
cd src/app
mkdir hooks
touch hooks/useDelete.js
touch hooks/useFetch.js
touch hooks/usePost.js
touch hooks/useUpdate.js
Right the folders are done lets quickly do some setup for Tailwind CSS and styling before we complete our codebase.
Replace all of the code in the globals.css
file with this code which just sets a background colour:
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
background: #eeeff1;
font-size: 16px;
color: #0e0e0e;
}
Now do the same for layout.js
we are just using the Arsenal font:
import { Arsenal } from 'next/font/google';
import './globals.css';
const arsenal = Arsenal({
weight: '400',
subsets: ['latin'],
});
export const metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={arsenal.className}>{children}</body>
</html>
);
}
Ok moving on let's get these hooks done. Up first is useFetch.js
give it this code:
import { useState, useEffect } from 'react';
export function useFetch(url) {
const [data, setData] = useState([]);
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const json = await fetch(url).then((r) => r.json());
setIsLoading(false);
setData(json);
} catch (error) {
setError(error);
setIsLoading(false);
}
};
fetchData();
const pollInterval = setInterval(() => {
fetchData();
}, 5000);
return () => {
clearInterval(pollInterval);
};
}, [url]);
return { data, error, isLoading };
}
Now for usePost
add this code to the file:
import { useState } from 'react';
export function usePost() {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [response, setResponse] = useState(null);
const postRequest = async (url, formData) => {
setIsLoading(true);
setError(null);
setResponse(null);
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
});
const responseData = await response.json();
if (response.ok) {
setResponse(responseData);
} else {
setError(responseData);
}
} catch (error) {
setError(error);
} finally {
setIsLoading(false);
}
};
return { isLoading, error, response, postRequest };
}
Next is useUpdate.js
and it gets this code:
import { useState } from 'react';
export function useUpdate() {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [response, setResponse] = useState(null);
const updateRequest = async (url, formData) => {
setIsLoading(true);
setError(null);
setResponse(null);
try {
const response = await fetch(url, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
});
const responseData = await response.json();
if (response.ok) {
setResponse(responseData);
} else {
setError(responseData);
}
} catch (error) {
setError(error);
} finally {
setIsLoading(false);
}
};
return { isLoading, error, response, updateRequest };
}
And lastly its useDelete.js
with this code here:
import { useState } from 'react';
export function useDelete() {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [response, setResponse] = useState(null);
const deleteRequest = async (url, formData) => {
setIsLoading(true);
setError(null);
setResponse(null);
try {
const response = await fetch(url, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
});
const responseData = await response.json();
if (response.ok) {
setResponse(responseData);
} else {
setError(responseData);
}
} catch (error) {
setError(error);
} finally {
setIsLoading(false);
}
};
return { isLoading, error, response, deleteRequest };
}
Finally, the page.js
file will receive this huge code snippet because we are not going to be using multiple component files in this quick tutorial:
'use client';
import { useState, useEffect } from 'react';
import { useFetch } from './hooks/useFetch';
import { usePost } from './hooks/usePost';
import { useUpdate } from './hooks/useUpdate';
import { useDelete } from './hooks/useDelete';
export default function Home() {
// GET API HOST URL
// Example fly.io online: https://fly-your-app-online.fly.dev/
// Local version: http://localhost:3000/
const API = 'http://localhost:3000/';
// POST form input state
const [firstname, setFirstname] = useState('');
const [lastname, setlastname] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
// UPDATE/PUT form input state
const [updateId, setUpdateId] = useState('');
const [updateFirstname, setUpdateFirstname] = useState('');
const [updateLastname, setUpdateLastname] = useState('');
const [updateEmail, setUpdateEmail] = useState('');
const [originalemail, setOriginalemail] = useState('');
const [updatePassword, setUpdatePassword] = useState('');
// DELETE form input state
const [deleteId, setDeleteId] = useState('');
// GET Route
const { data, error, isLoading } = useFetch(`${API}/api/getusers`);
if (error) return <div>An error has occurred.</div>;
if (isLoading) return <div>Loading...</div>;
useEffect(() => {
console.log('Client API GET Data:', data);
}, [data]);
const { postRequest } = usePost();
const { updateRequest } = useUpdate();
const { deleteRequest } = useDelete();
// CRUD message box state
const useToggleMessage = (initialState = 'hidden') => {
const [message, setMessage] = useState(initialState);
const toggleMessage = () => {
setMessage('');
setTimeout(() => {
setMessage('hidden');
}, 3000);
};
return [message, toggleMessage];
};
const [addUserMessage, setAddUserMessage] = useToggleMessage();
const [updateUserMessage, setUpdateUserMessage] = useToggleMessage();
const [deleteUserMessage, setDeleteUserMessage] = useToggleMessage();
const handlePostForm = async (e) => {
e.preventDefault();
if (
firstname === '' ||
lastname === '' ||
email === '' ||
password === ''
) {
console.log('The form needs all fields to be filled in');
} else {
try {
const user = {
firstname: firstname,
lastname: lastname,
email: email,
password: password,
};
// POST Route
postRequest(`${API}/api/postuser`, user);
console.log(`User ${user}`);
setFirstname('');
setlastname('');
setEmail('');
setPassword('');
setAddUserMessage();
} catch (error) {
console.log(error);
}
}
};
const handleUpdateForm = async (e) => {
e.preventDefault();
if (
updateId === '' ||
updateFirstname === '' ||
updateLastname === '' ||
originalemail === '' ||
updateEmail === '' ||
updatePassword === ''
) {
console.log('The form needs all fields to be filled in');
} else {
try {
const user = {
id: updateId,
firstname: updateFirstname,
lastname: updateLastname,
originalEmail: originalemail,
email: updateEmail,
password: updatePassword,
};
console.log(`User: ${user}`);
// UPDATE Route
updateRequest(`${API}/api/updateuser`, user);
setUpdateId('');
setUpdateFirstname('');
setUpdateLastname('');
setOriginalemail('');
setUpdateEmail('');
setUpdatePassword('');
setUpdateUserMessage();
} catch (error) {
console.log(error);
}
}
};
const handleDeleteForm = async (e) => {
e.preventDefault();
if (deleteId === '') {
console.log('Form needs an id to be submitted');
} else {
try {
const userId = {
id: deleteId,
};
console.log('User ID', userId);
// DELETE Route
deleteRequest(`${API}/api/deleteuser`, userId);
console.log(`User ${deleteId} deleted`);
console.log(`UserId ${userId}`);
setDeleteId('');
setDeleteUserMessage();
} catch (error) {
console.log(error);
}
}
};
return (
<div className="container mx-auto mt-4">
<h1 className="text-4xl mb-2 text-center uppercase">User Database</h1>
<div className="bg-gray-900 text-white p-4 rounded flex justify-center">
<table className="table-auto border border-slate-500">
<thead>
<tr>
<th className="border border-slate-600 p-2 text-2xl">ID</th>
<th className="border border-slate-600 p-2 text-2xl">
Firstname
</th>
<th className="border border-slate-600 p-2 text-2xl">Lastname</th>
<th className="border border-slate-600 p-2 text-2xl">Email</th>
</tr>
</thead>
{data === 0 ? (
<tbody></tbody>
) : (
<tbody>
{data.map((user) => (
<tr key={user.id}>
<td className="border border-slate-600 p-2 bg-gray-800 hover:bg-gray-600">
{user.id}
</td>
<td className="border border-slate-600 p-2 bg-gray-800 hover:bg-gray-600">
{user.firstname}
</td>
<td className="border border-slate-600 p-2 bg-gray-800 hover:bg-gray-600">
{user.lastname}
</td>
<td className="border border-slate-600 p-2 bg-gray-800 hover:bg-gray-600">
{user.email}
</td>
</tr>
))}
</tbody>
)}
</table>
</div>
<div className="bg-slate-100 rounded p-10 drop-shadow-lg">
<div className="bg-white p-4 rounded drop-shadow-md">
<h1 className="text-2xl mb-4">ADD User</h1>
<form onSubmit={(e) => handlePostForm(e)}>
<div className="flex flex-wrap items-center mb-2">
<label className="p-2 w-36 border-solid border-2">
Firstname
</label>
<input
type="text"
value={firstname}
onChange={(e) => setFirstname(e.target.value)}
className="grow p-2 border border-2"
required
/>
</div>
<div className="flex flex-wrap items-center mb-2">
<label className="p-2 w-36 border-solid border-2">Lastname</label>
<input
type="text"
value={lastname}
onChange={(e) => setlastname(e.target.value)}
className="grow p-2 border border-2"
required
/>
</div>
<div className="flex flex-wrap items-center mb-2">
<label className="p-2 w-36 border-solid border-2">Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="grow p-2 border border-2"
required
/>
</div>
<div className="flex flex-wrap items-center mb-2">
<label className="p-2 w-36 border-solid border-2">Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="grow p-2 border border-2"
required
/>
</div>
<div>
<button
type="submit"
className="bg-slate-600 hover:bg-slate-400 p-2 text-white cursor-pointer font-bold rounded-lg"
>
Add User
</button>
</div>
<div>
<p className={`bg-amber-100 p-2 mt-4 rounded ${addUserMessage}`}>
User added
</p>
</div>
</form>
</div>
<div className="bg-white p-4 rounded drop-shadow-md mb-4 mt-4">
<h1 className="text-2xl mb-4">UPDATE User</h1>
<form onSubmit={(e) => handleUpdateForm(e)}>
<div className="flex flex-wrap items-center mb-2">
<label className="p-2 w-36 border-solid border-2">ID</label>
<input
type="text"
value={updateId}
onChange={(e) => setUpdateId(e.target.value)}
className="grow p-2 border border-2"
required
/>
</div>
<div className="flex flex-wrap items-center mb-2">
<label className="p-2 w-36 border-solid border-2">
Firstname
</label>
<input
type="text"
value={updateFirstname}
onChange={(e) => setUpdateFirstname(e.target.value)}
className="grow p-2 border border-2"
required
/>
</div>
<div className="flex flex-wrap items-center mb-2">
<label className="p-2 w-36 border-solid border-2">Lastname</label>
<input
type="text"
value={updateLastname}
onChange={(e) => setUpdateLastname(e.target.value)}
className="grow p-2 border border-2"
required
/>
</div>
<div className="flex flex-wrap items-center mb-2">
<label className="p-2 w-36 border-solid border-2">
Original Email
</label>
<input
type="email"
value={originalemail}
onChange={(e) => setOriginalemail(e.target.value)}
className="grow p-2 border border-2"
required
/>
</div>
<div className="flex flex-wrap items-center mb-2">
<label className="p-2 w-36 border-solid border-2">Email</label>
<input
type="email"
value={updateEmail}
onChange={(e) => setUpdateEmail(e.target.value)}
className="grow p-2 border border-2"
required
/>
</div>
<div className="flex flex-wrap items-center mb-2">
<label className="p-2 w-36 border-solid border-2">Password</label>
<input
type="password"
value={updatePassword}
onChange={(e) => setUpdatePassword(e.target.value)}
className="grow p-2 border border-2"
required
/>
</div>
<div>
<button
type="submit"
className="bg-slate-600 hover:bg-slate-400 p-2 text-white cursor-pointer font-bold rounded-lg"
>
Update User
</button>
</div>
<div>
<p
className={`bg-amber-100 p-2 mt-4 rounded ${updateUserMessage}`}
>
User updated
</p>
</div>
</form>
</div>
<div className="bg-white p-4 rounded drop-shadow-md mb-4 mt-4">
<h1 className="text-2xl mb-4">DELETE User</h1>
<form onSubmit={(e) => handleDeleteForm(e)}>
<div className="flex flex-wrap items-center mb-2">
<label className="p-2 w-36 border-solid border-2">ID</label>
<input
type="text"
value={deleteId}
onChange={(e) => setDeleteId(e.target.value)}
className="grow p-2 border border-2"
required
/>
</div>
<div>
<button
type="submit"
className="bg-slate-600 hover:bg-slate-400 p-2 text-white cursor-pointer font-bold rounded-lg"
>
Delete User
</button>
</div>
<div>
<p
className={`bg-amber-100 p-2 mt-4 rounded ${deleteUserMessage}`}
>
User deleted
</p>
</div>
</form>
</div>
</div>
</div>
);
}
And we are done! Run the app if it's not running already with npm run dev
and give it a try!
Just a quick reminder that when using the UPDATE User form you need to be sure that you are using the Original Email otherwise it won't update. Also, be careful of the white space when copying the ID because it will also stop the updates from going through. Feel free to implement better error handling and checking if you want to ;)
Our app should be fully working so now we just need to deploy it online in the final section.
Deploying our app to fly.io
Deployment is the final step and you can read the documentation here https://fly.io/docs/apps/launch/ the commands should be fly launch
and then fly deploy
to get your app online.
Remember to add the environment variables in your .env.local
file to the Secrets page for your app on fly.io. Update the API HOST URL for the frontend routes inside of the main page.js
file when you deploy your app online. See the example code here:
// GET API HOST URL
// Example fly.io online: https://fly-your-app-online.fly.dev/
// Local version: http://localhost:3000/
const API = 'http://localhost:3000/';
Thats it deployment should be done as well and we can access our application online!
Conclusion
So today we learned how to build a full-stack application using Next.js and deploy it online to the app hosting platform fly.io. We also utilised Tigris for storing our user data in an AWS bucket online. The combination of both platforms makes them a very useful and powerful platform for getting our apps online. Both platforms offer many different features so it's worth playing around with them and seeing how they can be beneficial for your projects.