Introduction
Welcome to your first major project on Void Cloud! So far, we’ve explored the foundational concepts, set up our development environment, and deployed simple static sites and serverless functions. Now, it’s time to bring everything together and build a complete full-stack web application.
In this chapter, you’ll learn how to combine a modern frontend framework (Next.js) with Void Cloud’s powerful serverless functions to create an interactive web application. We’ll build a simple “Note-Taking App” that allows users to create, view, and delete notes. This project will solidify your understanding of how different components integrate within the Void Cloud ecosystem, from local development to seamless cloud deployment. Get ready to put your knowledge into practice and see your full-stack vision come to life!
To get the most out of this chapter, you should be familiar with:
- Basic JavaScript/TypeScript and React concepts.
- Working with the Void Cloud CLI (
voidctl). - Deploying static sites and serverless functions as covered in previous chapters.
Core Concepts
Building a full-stack application involves several moving parts. Let’s briefly recap the core concepts we’ll be leveraging:
The Full-Stack Architecture on Void Cloud
A typical full-stack application on Void Cloud often involves:
- Frontend: Your user interface, built with a framework like Next.js, React, Vue, or Svelte. This will be served as a static asset from Void Cloud’s global edge network.
- Backend: Your API logic, database interactions, and any server-side computations. On Void Cloud, this is often implemented using Serverless Functions (also known as “Void Functions” or “Edge Functions”). These functions run on demand, scale automatically, and are often deployed globally for low latency.
- Data Persistence: A database to store your application’s data. For simplicity in this initial project, we’ll start with an in-memory data store for the backend, but we’ll discuss how to integrate with external databases like PostgreSQL or MongoDB later.
Let’s visualize this architecture:
- User Browser: This is where your users interact with your application.
- Void Cloud Edge Network: Void Cloud automatically routes requests to the closest edge location, serving your static frontend files rapidly.
- Frontend App (Next.js): Your client-side application, built and optimized for fast loading and interactivity.
- API Call: When the frontend needs data or to perform server-side actions, it makes an API call.
- Void Function (Backend API): A serverless function that handles API requests, performs business logic, and interacts with the database. These scale automatically and only run when invoked.
- Data Store: Where your application’s persistent data lives. For this chapter, we’ll initially simulate this, but in a real-world scenario, you’d connect to a managed database service.
Next.js for the Frontend
Next.js is a React framework that enables powerful features like server-side rendering (SSR), static site generation (SSG), and API routes out of the box. For our project, it’s an excellent choice because:
- Integrated Development Experience: It handles bundling, compilation, and offers a great developer experience.
- Performance: Optimized for speed, delivering highly performant user interfaces.
- Easy Deployment: Void Cloud has first-class support for Next.js, detecting its configuration and deploying it efficiently.
Void Cloud Serverless Functions for the Backend
Void Cloud’s serverless functions are the backbone of our backend API. They offer several advantages:
- Automatic Scaling: Your functions scale from zero to thousands of requests without any manual intervention.
- Pay-per-use: You only pay for the compute time your functions actually consume.
- Global Deployment: Functions can be deployed to multiple regions, reducing latency for your users worldwide.
- Simplified Operations: Void Cloud handles the underlying infrastructure, patching, and scaling, letting you focus on your code.
In this project, we’ll define our API endpoints as individual Void Functions, each responsible for a specific operation (e.g., getting all notes, creating a note, deleting a note).
Step-by-Step Implementation: Building Our Note-Taking App
Let’s start building our full-stack note-taking application. We’ll keep it simple: create notes with a title and content, view all notes, and delete notes.
Step 1: Initialize Your Project Structure
We’ll create a monorepo-like structure where our frontend and backend code live in separate directories within a single Void Cloud project. This makes managing related services easier.
First, create a new directory for our project and navigate into it:
mkdir void-notes-app
cd void-notes-app
Now, let’s initialize the frontend and backend directories.
Step 2: Set Up the Frontend (Next.js)
We’ll use create-next-app to scaffold our Next.js frontend.
Create Next.js App: Run the following command in your
void-notes-appdirectory:npx create-next-app@latest frontend --typescript --eslint --tailwind --app --src-dir --use-pnpm@latest: Ensures we use the most current stable version ofcreate-next-app(as of 2026-03-14, this would be a recent stable release like Next.js v15.x or v16.x).frontend: This is the directory name for our frontend application.--typescript: To use TypeScript, a modern best practice.--eslint: For code quality.--tailwind: For styling (optional, but good for quick UI).--app: Uses the new App Router, which is the recommended approach for Next.js.--src-dir: Organizes code in asrcdirectory.--use-pnpm: Uses pnpm for faster and more efficient package management (you can usenpmoryarnif preferred by omitting this flag).
Follow the prompts. For most, you can accept the defaults.
Navigate and Test Frontend: Change into the
frontenddirectory and run the development server to ensure everything is set up correctly:cd frontend pnpm dev # or npm run dev / yarn devOpen
http://localhost:3000in your browser. You should see the default Next.js welcome page.Press
Ctrl+Cto stop the development server.Install
swrfor Data Fetching:swr(Stale-While-Revalidate) is a great React hook library for data fetching, caching, and revalidation. It simplifies interacting with our API.pnpm add swr # or npm install swr / yarn add swr
Step 3: Set Up the Backend (Void Cloud Serverless Functions)
Now, let’s create our backend functions. We’ll store them in a backend directory.
Create Backend Directory: Navigate back to the root of your project (
void-notes-app) and create abackenddirectory:cd .. # Go back to void-notes-app mkdir backend cd backendInitialize Node.js Project: Initialize a new Node.js project with TypeScript.
pnpm init # or npm init -y / yarn init -y pnpm add typescript @types/node ts-node # or npm install typescript @types/node ts-node pnpm add -D @void/functions-types # This is a hypothetical Void Cloud SDK type definition@void/functions-types: We’re assuming Void Cloud provides specific types for its function environment, similar to AWS Lambda or Vercel Functions.
Configure TypeScript: Create a
tsconfig.jsonfile in thebackenddirectory with basic configuration for serverless functions.// backend/tsconfig.json { "compilerOptions": { "target": "ES2022", // Modern ECMAScript target "module": "CommonJS", // Or ESNext if your Void functions support ESM directly "lib": ["ES2022"], "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "outDir": "./dist", // Output compiled JavaScript to 'dist' "rootDir": "./src" // Source files are in 'src' }, "include": ["src/**/*.ts"], "exclude": ["node_modules"] }Explanation: This configuration tells the TypeScript compiler how to process our
.tsfiles. We’re targeting a modern JavaScript version, using CommonJS modules (common for serverless runtimes), and enabling strict type checking.Create a
srcdirectory:mkdir srcCreate Our First Void Function (Get All Notes): Void Cloud functions are typically defined as individual files in a specific directory (e.g.,
src/api). Each file exports a handler function.Create
backend/src/api/notes/index.ts:mkdir -p src/api/notes touch src/api/notes/index.tsNow, open
backend/src/api/notes/index.tsand add the following code:// backend/src/api/notes/index.ts import { VoidFunctionRequest, VoidFunctionResponse } from '@void/functions-types'; // In-memory "database" for demonstration purposes interface Note { id: string; title: string; content: string; createdAt: string; } const notes: Note[] = []; // Our initial empty array of notes // Helper to generate unique IDs const generateId = () => Math.random().toString(36).substring(2, 9); export default async function handler( req: VoidFunctionRequest, res: VoidFunctionResponse ) { // Set CORS headers for local development and frontend access res.setHeader('Access-Control-Allow-Origin', '*'); // Allow all origins for now res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); // Handle preflight OPTIONS request if (req.method === 'OPTIONS') { return res.status(200).send(''); } if (req.method === 'GET') { // Get all notes return res.status(200).json(notes); } else if (req.method === 'POST') { // Create a new note const { title, content } = req.body; if (!title || !content) { return res.status(400).json({ error: 'Title and content are required.' }); } const newNote: Note = { id: generateId(), title, content, createdAt: new Date().toISOString(), }; notes.push(newNote); return res.status(201).json(newNote); } else if (req.method === 'DELETE') { // Delete a note by ID const { id } = req.query; // Assuming ID comes as a query parameter for DELETE const initialLength = notes.length; const noteIndex = notes.findIndex(note => note.id === id); if (noteIndex === -1) { return res.status(404).json({ error: 'Note not found.' }); } notes.splice(noteIndex, 1); // Remove the note return res.status(204).send(''); // 204 No Content for successful deletion } // If method is not handled return res.status(405).json({ error: 'Method Not Allowed' }); }Explanation:
- We import
VoidFunctionRequestandVoidFunctionResponsetypes to ensure type safety for our handler. notesis a simple in-memory array acting as our temporary database. Important: In a real serverless environment, this array would reset with each function invocation, meaning data isn’t persistent. We’ll address this by discussing real databases later. For local testing, it will persist as long as thets-nodeprocess runs.- The
handlerfunction checksreq.methodto determine if it’s aGET,POST, orDELETErequest. - CORS Headers: These are crucial for local development, allowing our frontend (running on
localhost:3000) to make requests to our backend (which will run on a different port/domain). For production, you’d restrictAccess-Control-Allow-Originto your frontend’s domain. GET /api/notes: Returns all notes.POST /api/notes: Creates a new note from the request body.DELETE /api/notes?id=...: Deletes a note by its ID.OPTIONS: Handles CORS preflight requests.
- We import
Test Backend Locally: We can use
ts-nodeto run our function locally as a simple HTTP server. This isn’t how Void Cloud runs it, but it’s great for quick local testing.First, install
expressandbody-parserto simulate a basic HTTP server for local testing:pnpm add express body-parser # or npm install express body-parser pnpm add -D @types/express @types/body-parser # or npm install -D @types/express @types/body-parserCreate a file
backend/dev-server.tsto simulate the Void Cloud function environment:// backend/dev-server.ts import express from 'express'; import bodyParser from 'body-parser'; import handler from './src/api/notes'; // Our Void Function handler const app = express(); const port = 4000; // Choose a different port than frontend app.use(bodyParser.json()); // Simulate Void Cloud's routing for /api/notes app.all('/api/notes', async (req, res) => { // Simulate VoidFunctionRequest and VoidFunctionResponse structure const voidReq: any = { method: req.method, headers: req.headers, body: req.body, query: req.query, url: req.url, }; const voidRes: any = { status: (statusCode: number) => { res.status(statusCode); return voidRes; // Chainable }, json: (data: any) => { res.json(data); return voidRes; }, send: (data: string | undefined) => { res.send(data); return voidRes; }, setHeader: (name: string, value: string) => { res.setHeader(name, value); return voidRes; } }; await handler(voidReq, voidRes); }); app.listen(port, () => { console.log(`Backend dev server running on http://localhost:${port}`); console.log('Test endpoints: GET /api/notes, POST /api/notes, DELETE /api/notes?id=...'); });Explanation: This
dev-server.tsfile sets up a simple Express server. It intercepts all requests to/api/notesand passes them to ourhandlerfunction, simulating how Void Cloud would invoke it.Now, run the backend dev server:
pnpm ts-node dev-server.ts # or npx ts-node dev-server.tsYou should see “Backend dev server running on http://localhost:4000”.
Test it using
curlor Postman/Insomnia:GET http://localhost:4000/api/notes(should return[])POST http://localhost:4000/api/notes -H "Content-Type: application/json" -d '{"title": "My First Note", "content": "This is the content."}'GET http://localhost:4000/api/notes(should now show your note)DELETE http://localhost:4000/api/notes?id=<ID_OF_YOUR_NOTE>(replace<ID_OF_YOUR_NOTE>with an actual note ID)
Keep this backend server running in a separate terminal tab.
Step 4: Integrate Frontend with Backend
Now that our backend API is ready, let’s connect our Next.js frontend to it.
Define API Endpoint: Go back to your
frontenddirectory. We’ll define a constant for our backend API URL. This is important because it will be different in development vs. production.Create
frontend/src/lib/constants.ts:cd ../frontend mkdir -p src/lib touch src/lib/constants.ts// frontend/src/lib/constants.ts export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:4000/api';Explanation:
process.env.NEXT_PUBLIC_API_BASE_URL: This is how Next.js handles environment variables that need to be exposed to the browser. In production, Void Cloud will inject this.|| 'http://localhost:4000/api': In local development, we’ll default to our local backend server.
Create Note Component: We’ll create a simple component to display a single note and handle its deletion.
Create
frontend/src/components/NoteCard.tsx:// frontend/src/components/NoteCard.tsx interface Note { id: string; title: string; content: string; createdAt: string; } interface NoteCardProps { note: Note; onDelete: (id: string) => void; } export default function NoteCard({ note, onDelete }: NoteCardProps) { return ( <div className="border p-4 rounded-md shadow-sm bg-white mb-4"> <h3 className="text-xl font-semibold mb-2">{note.title}</h3> <p className="text-gray-700 mb-4">{note.content}</p> <p className="text-sm text-gray-500">Created: {new Date(note.createdAt).toLocaleString()}</p> <button onClick={() => onDelete(note.id)} className="mt-4 px-4 py-2 bg-red-500 text-white rounded-md hover:bg-red-600 transition-colors" > Delete </button> </div> ); }Update Main Page to Fetch and Display Notes: Now, let’s modify
frontend/src/app/page.tsxto fetch notes from our backend and display them. We’ll also add a form to create new notes.// frontend/src/app/page.tsx 'use client'; // This directive makes it a Client Component import { useState } from 'react'; import useSWR from 'swr'; import NoteCard from '../components/NoteCard'; import { API_BASE_URL } from '../lib/constants'; interface Note { id: string; title: string; content: string; createdAt: string; } // A simple fetcher function for SWR const fetcher = (url: string) => fetch(url).then(res => res.json()); export default function Home() { const { data: notes, error, isLoading, mutate } = useSWR<Note[]>(`${API_BASE_URL}/notes`, fetcher); const [newNoteTitle, setNewNoteTitle] = useState(''); const [newNoteContent, setNewNoteContent] = useState(''); const handleCreateNote = async (e: React.FormEvent) => { e.preventDefault(); if (!newNoteTitle || !newNoteContent) return; try { await fetch(`${API_BASE_URL}/notes`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: newNoteTitle, content: newNoteContent }), }); setNewNoteTitle(''); setNewNoteContent(''); mutate(); // Revalidate SWR cache to fetch updated notes } catch (err) { console.error('Failed to create note:', err); } }; const handleDeleteNote = async (id: string) => { try { await fetch(`${API_BASE_URL}/notes?id=${id}`, { method: 'DELETE', }); mutate(); // Revalidate SWR cache } catch (err) { console.error('Failed to delete note:', err); } }; if (error) return <div className="text-red-500">Failed to load notes: {error.message}</div>; if (isLoading) return <div className="text-blue-500">Loading notes...</div>; return ( <div className="container mx-auto p-4 max-w-2xl"> <h1 className="text-4xl font-bold mb-8 text-center text-gray-800">Void Notes App</h1> <form onSubmit={handleCreateNote} className="bg-gray-100 p-6 rounded-lg shadow-md mb-8"> <h2 className="text-2xl font-semibold mb-4">Create New Note</h2> <div className="mb-4"> <label htmlFor="title" className="block text-gray-700 text-sm font-bold mb-2">Title</label> <input type="text" id="title" value={newNoteTitle} onChange={(e) => setNewNoteTitle(e.target.value)} className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" placeholder="Note title" required /> </div> <div className="mb-6"> <label htmlFor="content" className="block text-gray-700 text-sm font-bold mb-2">Content</label> <textarea id="content" value={newNoteContent} onChange={(e) => setNewNoteContent(e.target.value)} className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline h-24 resize-none" placeholder="Write your note here..." required /> </div> <button type="submit" className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline" > Add Note </button> </form> <h2 className="text-3xl font-bold mb-6 text-gray-800">Your Notes</h2> <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> {notes && notes.length > 0 ? ( notes.map((note) => ( <NoteCard key={note.id} note={note} onDelete={handleDeleteNote} /> )) ) : ( <p className="text-gray-600 col-span-full text-center">No notes yet. Start by creating one above!</p> )} </div> </div> ); }Explanation:
'use client';: This directive is crucial for Next.js App Router to indicate that this component runs on the client side and can use React hooks likeuseStateanduseSWR.useSWR: This hook handles fetching notes from ourAPI_BASE_URL/notesendpoint. It also providesmutateto easily re-fetch data after creating or deleting a note.handleCreateNoteandhandleDeleteNote: These functions makePOSTandDELETErequests to our backend API, respectively, and then callmutate()to update the UI with the latest data.- The UI includes a form for adding notes and a list that maps
notesdata toNoteCardcomponents.
Run Both Frontend and Backend Locally: Make sure your
backend/dev-server.tsis running in one terminal:cd void-notes-app/backend pnpm ts-node dev-server.tsIn a separate terminal, start the Next.js frontend:
cd void-notes-app/frontend pnpm devNow, open
http://localhost:3000. You should be able to create and delete notes, with the data appearing and disappearing as you interact with the app. Remember that the backend’s in-memory storage resets if you restartdev-server.ts.
Step 5: Deploy to Void Cloud
Now for the exciting part: deploying our full-stack application to Void Cloud!
Configure Void Cloud Project: Void Cloud uses a configuration file (typically
void.jsonorvoid.yaml) at the root of your project to understand how to deploy different parts of your application.Navigate back to the root of your project (
void-notes-app):cd .. # From frontend to void-notes-appCreate a
void.jsonfile:// void-notes-app/void.json { "version": 3, "name": "void-notes-app", "builds": [ { "src": "frontend/package.json", "use": "@void/next-builder" }, { "src": "backend/src/api/**/*.ts", "use": "@void/node-serverless-builder", "config": { "includeFiles": ["backend/package.json", "backend/pnpm-lock.yaml", "backend/tsconfig.json"] } } ], "routes": [ { "src": "/api/(.*)", "dest": "backend/src/api/$1" }, { "src": "/(.*)", "dest": "frontend/$1" } ], "env": { "NEXT_PUBLIC_API_BASE_URL": "https://void-notes-app-YOURPROJECTID.void.app/api" } }Explanation:
"version": 3: Specifies the Void Cloud configuration format version."name": "void-notes-app": The name of your project on Void Cloud."builds": This array tells Void Cloud how to build different parts of your application.- The first entry specifies that anything related to
frontend/package.jsonshould use the@void/next-builder. Void Cloud will automatically detect and build your Next.js application. - The second entry tells Void Cloud to treat all
.tsfiles underbackend/src/apias serverless functions, using the@void/node-serverless-builder. "config": { "includeFiles": [...] }: This is crucial! Since ourbackenddirectory is not the root of the project, we need to explicitly tell the builder to include necessary files likepackage.json(for dependencies),pnpm-lock.yaml(for exact dependency versions), andtsconfig.json(for TypeScript compilation) from thebackenddirectory.
- The first entry specifies that anything related to
"routes": This defines how incoming requests are routed to your builds."src": "/api/(.*)", "dest": "backend/src/api/$1": Any request starting with/api/will be routed to our backend serverless functions. The(.*)captures the rest of the path and$1passes it to the function. So/api/notesroutes tobackend/src/api/notes/index.ts."src": "/(.*)", "dest": "frontend/$1": All other requests (that don’t match/api/) are routed to the frontend application.
"env": This section defines environment variables."NEXT_PUBLIC_API_BASE_URL": This is the production URL for our backend API. You’ll need to replaceYOURPROJECTIDwith the actual project ID or deployment URL Void Cloud gives you after the first deployment. For now, use a placeholder. Void Cloud automatically provides a unique subdomain for each project.- Important Note on
NEXT_PUBLIC_API_BASE_URL: During the first deployment, you won’t know the exact URL. A common practice is to deploy once, get the URL, then updatevoid.jsonand redeploy. For simplicity, we’ll assume a pattern:https://[PROJECT_NAME]-[RANDOM_ID].void.app/api. Void Cloud’s CLI usually gives you the deployment URL after a successful deployment. We’ll simulate this.
Login to Void Cloud CLI: If you haven’t already, log in to your Void Cloud account using the CLI:
voidctl loginThis will open your browser for authentication.
Deploy the Project: From the root of your
void-notes-appdirectory, initiate the deployment:voidctl deployThe CLI will guide you through the initial setup if this is your first time deploying this project:
- It might ask “Set up and deploy
void-notes-appto Void Cloud?” (Y/n) -> TypeY. - “Which scope (organization/personal account) do you want to deploy to?” -> Select your personal account or team.
- “Link to existing project?” (Y/n) -> Type
n(this is a new project). - “What’s your project’s name?” -> Default
void-notes-app(press Enter). - “In which directory is your code located?” -> Default
.(press Enter, asvoid.jsonis at the root).
Void Cloud will then analyze your
void.json, build your frontend and backend functions, and deploy them. This process might take a few minutes.Once complete, you’ll see a production URL like
https://void-notes-app-xxxxxx.void.app(wherexxxxxxis a unique ID). CRITICAL: Copy this URL!- It might ask “Set up and deploy
Update
NEXT_PUBLIC_API_BASE_URLand Redeploy: Now that you have the production URL, updatevoid.jsonwith the correctNEXT_PUBLIC_API_BASE_URL. For example, if your deployment URL washttps://void-notes-app-abc12345.void.app, then yourAPI_BASE_URLwould behttps://void-notes-app-abc12345.void.app/api.// void-notes-app/void.json (updated) { "version": 3, "name": "void-notes-app", "builds": [ { "src": "frontend/package.json", "use": "@void/next-builder" }, { "src": "backend/src/api/**/*.ts", "use": "@void/node-serverless-builder", "config": { "includeFiles": ["backend/package.json", "backend/pnpm-lock.yaml", "backend/tsconfig.json"] } } ], "routes": [ { "src": "/api/(.*)", "dest": "backend/src/api/$1" }, { "src": "/(.*)", "dest": "frontend/$1" } ], "env": { "NEXT_PUBLIC_API_BASE_URL": "https://void-notes-app-abc12345.void.app/api" // <-- UPDATE THIS! } }Save the file, and then redeploy:
voidctl deployThis second deployment will be faster as Void Cloud caches builds. Once complete, open the new production URL in your browser. You should now have a fully functional note-taking app deployed on Void Cloud!
Remember: Since our backend uses in-memory storage, notes will not persist across function invocations or deployments. We’ll tackle persistent storage in a later chapter.
Mini-Challenge: Add a “Clear All Notes” Button
Let’s enhance our note-taking app with a new feature.
Challenge:
Add a button to the frontend that, when clicked, sends a DELETE request to a new Void Cloud backend function /api/notes/clear to remove all notes.
Hints:
- Backend: Create a new file
backend/src/api/notes/clear.ts. This function should export a handler that, when invoked with aDELETErequest, empties thenotesarray. Remember that thenotesarray is scoped toindex.ts, so you’ll need to refactor it into a separate module (e.g.,backend/src/lib/data.ts) and import it into bothindex.tsandclear.ts. - Frontend: Add a new button to
frontend/src/app/page.tsxand implement anonClickhandler that sends aDELETErequest toAPI_BASE_URL/notes/clear. Don’t forget tomutate()the SWR cache after the operation. - Deployment: You shouldn’t need to change
void.jsonas thebackend/src/api/**/*.tsglob pattern will automatically pick up your new function. Just runvoidctl deployagain after making changes.
What to Observe/Learn:
- How to extend your backend with new serverless functions.
- How to refactor shared logic (like our in-memory data store) between functions.
- The ease of adding new API endpoints and connecting them to the frontend.
- Void Cloud’s automatic detection and deployment of new functions.
Common Pitfalls & Troubleshooting
CORS Issues (Cross-Origin Resource Sharing):
- Symptom: Your frontend makes requests to the backend, but you see errors like “Access to fetch has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present…”
- Cause: The browser prevents a web page from making requests to a different domain (or port) than the one it originated from unless the server explicitly allows it via CORS headers.
- Solution: Ensure your backend functions (
backend/src/api/notes/index.ts) setAccess-Control-Allow-Origin: *during local development. For production, replace*with your actual frontend domain (e.g.,https://void-notes-app-abc12345.void.app). Also, ensureAccess-Control-Allow-MethodsandAccess-Control-Allow-Headersare correctly set, andOPTIONSrequests are handled.
In-Memory Data Not Persisting in Production:
- Symptom: You create notes, but after refreshing the page or waiting a bit, they disappear.
- Cause: Void Cloud serverless functions are ephemeral. Each invocation might run on a fresh instance, and any in-memory state (like our
notesarray) is lost between invocations. - Solution: This is expected for an in-memory store. For persistent data, you must integrate with an external database service (e.g., PostgreSQL, MongoDB, Redis, or a Void Cloud Database offering if available). We will cover this in detail in a future chapter.
Incorrect
NEXT_PUBLIC_API_BASE_URL:- Symptom: Frontend fails to fetch data from the backend after deployment, but works locally. Network requests show 404s or connection errors to the backend API.
- Cause: The
NEXT_PUBLIC_API_BASE_URLenvironment variable in yourvoid.json(or the local default) points to the wrong URL. - Solution: Double-check the deployment URL provided by
voidctl deployand ensure it’s correctly updated invoid.jsonunder theenvsection. Remember to redeploy after updatingvoid.json.
Backend Dependencies Not Found:
- Symptom: Deployment fails or backend function logs show errors about missing modules (e.g., “Cannot find module ’express’”).
- Cause: The
@void/node-serverless-builderdidn’t correctly pick up yourbackend/package.jsonto install dependencies. - Solution: Verify the
config.includeFilesarray in yourvoid.jsonfor the backend build step. It must explicitly listbackend/package.jsonand any lock files (pnpm-lock.yaml,package-lock.json,yarn.lock).
Summary
Congratulations! You’ve just built and deployed your first full-stack web application on Void Cloud. This is a significant milestone!
Here’s what we covered in this chapter:
- Full-Stack Architecture: Understanding how frontend (Next.js), backend (Void Cloud Serverless Functions), and data persistence work together.
- Frontend Development: Setting up a Next.js application, fetching data with
useSWR, and interacting with a backend API. - Backend Development: Creating Void Cloud serverless functions with TypeScript, handling different HTTP methods (GET, POST, DELETE), and managing a simple in-memory data store.
- Local Development Workflow: Running both frontend and backend locally for rapid iteration.
- Void Cloud Deployment: Configuring
void.jsonto deploy a multi-service application (static frontend + serverless backend) and usingvoidctl deploy. - Environment Variables: Managing API endpoints for different environments using
NEXT_PUBLIC_API_BASE_URL.
While our note-taking app uses in-memory storage for simplicity, you now have a solid foundation. In upcoming chapters, we’ll dive into integrating real databases, implementing user authentication, and exploring more advanced Void Cloud features to build truly production-ready applications.
References
- Void Cloud Documentation: Getting Started with Full-Stack Applications (Hypothetical)
- Void Cloud Documentation: Serverless Functions (Hypothetical)
- Next.js Documentation
- SWR - React Hooks for Data Fetching
- TypeScript Handbook
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.