An open API service indexing awesome lists of open source software.

https://github.com/hngprojects/open-profile-fe


https://github.com/hngprojects/open-profile-fe

Last synced: 12 days ago
JSON representation

Awesome Lists containing this project

README

          

# Next.js Starter

Next.js 16 + React 19 + Tailwind v4 + shadcn (radix-maia). Validated env, typed proxy, and the standard set of route conventions wired up.

## Stack

- **Next.js 16** App Router (`proxy.ts`, `forbidden.tsx`, `unauthorized.tsx`)
- **React 19**, **TypeScript** (strict)
- **Tailwind v4** with shadcn `radix-maia` style
- **`@t3-oss/env-nextjs`** + **Zod 4** for build-time env validation

## Getting started

```bash
pnpm install
cp .env.example .env.local # fill in values
pnpm dev
```

Open .

## Scripts

| Command | What it does |
| ---------------- | -------------------------------- |
| `pnpm dev` | Dev server |
| `pnpm build` | Production build (validates env) |
| `pnpm start` | Run the production build |
| `pnpm lint` | ESLint |
| `pnpm typecheck` | `tsc --noEmit` |

## Environment variables

Schemas live in [`src/env/`](./src/env), split by side:

- [`src/env/server.ts`](./src/env/server.ts) — server-only vars. t3-env throws at runtime if a client component reads it.
- [`src/env/client.ts`](./src/env/client.ts) — `NEXT_PUBLIC_*` vars, safe everywhere.

Both are imported in [`next.config.ts`](./next.config.ts) so the build fails on any malformed value. Set `SKIP_ENV_VALIDATION=1` to bypass (Docker, lint-only CI).

| Var | Side | Required | Notes |
| ---------------------- | ------ | -------- | ------------------------------------- |
| `NODE_ENV` | server | auto | `development` / `test` / `production` |
| `API_BASE_URL` | server | optional | Upstream API for server-side `fetch` |
| `API_SECRET` | server | optional | Bearer token forwarded server-side |
| `NEXT_PUBLIC_APP_URL` | client | optional | Defaults to `http://localhost:3000` |
| `NEXT_PUBLIC_APP_NAME` | client | optional | Defaults to `Next Starter` |

Use it like:

```ts
// Server code (route handlers, Server Components, Server Actions)
import { env } from "@/env/server";

await fetch(`${env.API_BASE_URL}/users`, {
headers: { Authorization: `Bearer ${env.API_SECRET}` },
});

// Client code or shared metadata
import { env } from "@/env/client";

console.log(env.NEXT_PUBLIC_APP_URL);
```

## Proxy (`src/proxy.ts`)

Replaces the legacy `middleware.ts` (Next.js 16 renamed it). It runs before the cache and:

- Generates an `x-request-id` and forwards it to the request headers + response
- Sets baseline security headers (`X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy`, `Permissions-Policy`)
- Skips static assets via the matcher

Add auth gating, rewrites, or redirects there as needed. Note: `runtime` config is **not** allowed in `proxy.ts` — it always runs on Node.js.

## Route conventions wired up

| File | Purpose |
| ----------------------------- | ---------------------------------------- |
| `src/app/loading.tsx` | Root suspense fallback |
| `src/app/error.tsx` | Client error boundary (`unstable_retry`) |
| `src/app/not-found.tsx` | 404 page |
| `src/app/forbidden.tsx` | 403 page (calls `forbidden()`) |
| `src/app/unauthorized.tsx` | 401 page (calls `unauthorized()`) |
| `src/app/robots.ts` | `/robots.txt` |
| `src/app/sitemap.ts` | `/sitemap.xml` |
| `src/app/api/health/route.ts` | Liveness probe at `GET /api/health` |

`forbidden.tsx` and `unauthorized.tsx` require `experimental.authInterrupts: true`, already enabled in [`next.config.ts`](./next.config.ts).

## API layer (`src/api`)

All outbound HTTP calls to the backend, their TypeScript types, and **TanStack React Query option objects** belong under [`src/api`](./src/api). Prefer one folder per feature (for example [`src/api/auth/`](./src/api/auth)), with naming like:

| File | Responsibility |
| -------------- | -------------------------------------------------- |
| `*.service.ts` | Thin functions that call `callApi` (or similar) |
| `*.type.ts` | Request/response DTO types |
| `*.options.ts` | `mutationOptions` / `queryOptions` for React Query |

Shared helpers (`callApi`, errors, staleness presets) live in [`src/api/base`](./src/api/base). For queries that depend on route params or other inputs, set `enabled` with [`isQueryEnabled`](./src/api/base/base.util.ts) **inside** the corresponding `*.options.ts` factory so components stay dumb.

### Client-side requests

In **Client Components** (or other browser code), do not call services directly for data that should be cached, deduped, or retried. Use **TanStack React Query** and import the option object from the feature’s `*.options.ts` file.

- **Mutations** — export a **constant** created with `mutationOptions` (no parameters needed for the option itself).
- **Queries** — export a **function** that returns `queryOptions(...)`, so callers can pass IDs, filters, or other inputs into `queryKey`, `queryFn`, and **`enabled`** (typically via **`isQueryEnabled`** so the query skips until inputs are ready).

Examples:

```tsx
"use client";

import { useMutation, useQuery } from "@tanstack/react-query";
import { loginOption, getCurrentUserOption } from "@/api/auth/auth.options";

// Mutation: pass the exported option directly
const login = useMutation(loginOption);

// Query: invoke the option factory
const userQuery = useQuery(getCurrentUserOption());
```

When a query should only run after required inputs exist, encode that in **the options factory**, not at each `useQuery` call site. Use **`isQueryEnabled`** (from [`base.util.ts`](./src/api/base/base.util.ts)): it accepts a string, optional primitive, or a record of param values:

```ts
import { fetchUser } from "@/api/users/user.service";
import { isQueryEnabled } from "@/api/base/base.util";
import { queryOptions } from "@tanstack/react-query";

export function fetchUserOption(userId: string | undefined) {
return queryOptions({
queryKey: ["users", userId],
enabled: isQueryEnabled(userId),
// `queryFn` runs only when `enabled` is true
queryFn: ({ signal }) => fetchUser(userId!, { signal }),
});
}
```

```tsx
"use client";

import { useQuery } from "@tanstack/react-query";
import { fetchUserOption } from "@/api/users/user.options"; // illustrative

type Props = { userId: string | undefined };

export function UserPanel({ userId }: Props) {
const query = useQuery(fetchUserOption(userId));
// ...
}
```

Keep `queryFn` implementations in services (`*.service.ts`); options files wire keys, staleness, `enabled` / `isQueryEnabled`, and `mutationFn` / `queryFn` to those functions.

## Project layout

```
src/
├── api/ # Backend API: services, types, React Query options
│ ├── base/ # callApi, shared types/errors, staleness utils
│ └── auth/ # Example feature slice
├── app/ # App Router routes & file conventions
│ └── api/health/ # Liveness probe
├── components/ui/ # shadcn components (added via `pnpm dlx shadcn@latest add ...`)
├── lib/utils.ts # cn() helper
├── env/
│ ├── server.ts # Server-only env schema
│ └── client.ts # NEXT_PUBLIC_* env schema
└── proxy.ts # Next.js 16 proxy (formerly middleware)
```