Overview
The Projects API allows freelancers to create, update, and manage portfolio projects that showcase their previous work. Projects include title, description, and a gallery of images, videos, and documents.
All project endpoints require authentication. Users can only manage their own projects.
Project Model
Unique project identifier (cuid)
Project title (5-250 characters)
Project description (5-2000 characters)
Publication status: draft or published
Project media files Array of image URLs (at least 1 required)
ID of the user who created the project
Mutations
Create Draft
Create a new empty project draft. This is the first step in the project creation workflow.
const project = await trpc . project . createDraft . mutate ();
Response
Returns a new Project object with empty title and description:
{
"id" : "clx1234567890" ,
"title" : "" ,
"description" : "" ,
"status" : "draft" ,
"userId" : "user123" ,
"createdAt" : "2024-03-04T10:00:00Z" ,
"updatedAt" : "2024-03-04T10:00:00Z"
}
Create/Update Project
Update an existing project draft with complete details and publish it.
Input Parameters
Project title (5-250 characters)
Project description (5-2000 characters)
Either "draft" or "published"
Project media files Image URLs (minimum 1 required)
const project = await trpc . project . createProject . mutate ({
id: "clx1234567890" ,
title: "E-commerce Website for Fashion Brand" ,
description: "Built a full-featured online store with payment integration..." ,
status: "published" ,
gallery: {
images: [
"https://bucket.s3.amazonaws.com/uuid1/homepage.jpg" ,
"https://bucket.s3.amazonaws.com/uuid2/product-page.jpg"
],
videos: [
"https://bucket.s3.amazonaws.com/uuid3/demo-video.mp4"
],
documents: []
}
});
Process
Validates project ownership (must be owned by current user)
Deletes all existing attachments for the project
Updates project title, description, and status
Creates new attachment records for all gallery items
Returns updated project
Error Handling
Throws NOT_FOUND error if project doesn’t exist or doesn’t belong to current user.
Delete Project
Delete a project and all its associated attachments.
Input Parameters
await trpc . project . delete . mutate ({
id: "clx1234567890"
});
Process
Validates project ownership
Deletes all attachments
Deletes the project
Returns the deleted project object
This action is permanent and cannot be undone. Throws NOT_FOUND if project doesn’t exist or doesn’t belong to current user.
Server Functions
In addition to tRPC procedures, the project router exports server-side functions for server components and server actions.
getProjectDetails
Retrieve a single project with formatted gallery.
import { getProjectDetails } from "@/server/api/routers/project" ;
const project = await getProjectDetails ( projectId , userId );
getUserProjects
Retrieve all projects for a user with formatted galleries.
import { getUserProjects } from "@/server/api/routers/project" ;
const projects = await getUserProjects ( userId );
Return Type
type TGetUserProjects = {
id : string ;
title : string ;
description : string ;
status : "draft" | "published" ;
userId : string ;
createdAt : Date ;
gallery : {
images : string [];
videos : string [];
documents : string [];
};
}[];
Complete Workflow Example
import { api } from "@/trpc/react" ;
function CreateProjectForm () {
const createDraft = api . project . createDraft . useMutation ();
const updateProject = api . project . createProject . useMutation ();
const deleteProject = api . project . delete . useMutation ();
async function handleCreateProject ( formData : any ) {
try {
// Step 1: Create empty draft
const draft = await createDraft . mutateAsync ();
// Step 2: Upload images using file router
const imageUrls = await Promise . all (
formData . images . map ( async ( file : File ) => {
const { url , key } = await api . file . generateUrl . mutate ({
filename: file . name ,
filetype: file . type ,
});
await fetch ( url , {
method: "PUT" ,
body: file ,
headers: { "Content-Type" : file . type },
});
return `https://bucket.s3.amazonaws.com/ ${ key } ` ;
})
);
// Step 3: Update and publish project
const project = await updateProject . mutateAsync ({
id: draft . id ,
title: formData . title ,
description: formData . description ,
status: "published" ,
gallery: {
images: imageUrls ,
videos: [],
documents: [],
},
});
console . log ( "Project created:" , project );
} catch ( error ) {
console . error ( "Error creating project:" , error );
}
}
return (
< form onSubmit = { handleCreateProject } >
{ /* Form fields */ }
</ form >
);
}
Validation Schema
From ~/workspace/source/src/schemas:
const GallerySchema = z . object ({
images: z . array ( z . string ()). refine (( images ) => images . length > 0 , {
message: "At least one image is required" ,
}),
videos: z . array ( z . string ()),
documents: z . array ( z . string ()),
});
const projectSchema = z . object ({
title: z . string (). min ( 5 , "Title is too short" ). max ( 250 , "Title is too long" ),
description: z
. string ()
. min ( 5 , "Description is too short" )
. max ( 2000 , "Description is too long" ),
gallery: GallerySchema ,
});
Database Schema
From ~/workspace/source/prisma/schema.prisma:
model Project {
id String @id @default ( cuid ())
title String @db.VarChar ( 255 )
description String @db.Text
status ProjectStatus @default ( draft )
gallery Attachement []
createdAt DateTime @default ( now ()) @map ( "created_at" )
updatedAt DateTime @updatedAt @map ( "updated_at" )
user User ? @relation ( fields : [ userId ], references : [ id ] )
userId String ? @map ( "user_id" )
@@map ( "projects" )
}
enum ProjectStatus {
draft
published
}
model Attachement {
id String @id @default ( cuid ())
name String
url String
type String
userId String ? @map ( "user_id" )
user User ? @relation ( fields : [ userId ], references : [ id ] )
projectId String ? @map ( "project_id" )
project Project ? @relation ( fields : [ projectId ], references : [ id ] )
createdAt DateTime @default ( now ()) @map ( "created_at" )
updatedAt DateTime @updatedAt @map ( "updated_at" )
@@map ( "attachements" )
}
Related Pages