Quick Start
Let's build a simple feature to understand how NextShip works.
Understanding the Stack
NextShip uses these core technologies:
| Layer | Technology | Purpose |
|---|---|---|
| Frontend | React 19 + Next.js 15 | UI and routing |
| Styling | Tailwind CSS v4 + shadcn/ui | Components and styling |
| API | tRPC + Server Actions | Type-safe API calls |
| Database | Drizzle ORM + PostgreSQL | Data persistence |
| Auth | Better Auth | User authentication |
Adding a New Page
- Create a new file in
src/app/[locale]/(dashboard)/:
// src/app/[locale]/(dashboard)/my-page/page.tsx
import { setRequestLocale } from "next-intl/server";
export default async function MyPage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
setRequestLocale(locale);
return (
<div className="container py-8">
<h1 className="text-2xl font-bold">My New Page</h1>
</div>
);
}Adding a Database Table
- Define the schema in
src/lib/db/schema.ts:
export const projects = pgTable("projects", {
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
name: text("name").notNull(),
userId: text("user_id").references(() => users.id),
createdAt: timestamp("created_at").defaultNow(),
});- Push the changes:
pnpm db:pushCreating a tRPC Router
- Add a new router in
src/server/trpc/routers/:
// src/server/trpc/routers/project.ts
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { projects } from "@/lib/db/schema";
export const projectRouter = createTRPCRouter({
list: protectedProcedure.query(async ({ ctx }) => {
return ctx.db
.select()
.from(projects)
.where(eq(projects.userId, ctx.user.id));
}),
create: protectedProcedure
.input(z.object({ name: z.string().min(1) }))
.mutation(async ({ ctx, input }) => {
return ctx.db.insert(projects).values({
name: input.name,
userId: ctx.user.id,
});
}),
});- Register in the root router (
src/server/trpc/router.ts).
Using the API in Components
"use client";
import { trpc } from "@/lib/trpc/client";
export function ProjectList() {
const { data: projects, isLoading } = trpc.project.list.useQuery();
if (isLoading) return <div>Loading...</div>;
return (
<ul>
{projects?.map((project) => (
<li key={project.id}>{project.name}</li>
))}
</ul>
);
}Next Steps
- Tech Stack - Deep dive into the technologies
- Authentication - User management
- Payments - Stripe integration