Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --api Flag to Create Headless API App with create-next-app #68130

Open
wants to merge 65 commits into
base: canary
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 50 commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
513d424
add: headless-api
Arindam200 Jul 24, 2024
63fb471
add: Readme Templates
Arindam200 Jul 24, 2024
632b11b
fix: --api flag
Arindam200 Jul 25, 2024
393b432
Update packages/create-next-app/create-app.ts
Arindam200 Jul 25, 2024
76057cd
Update packages/create-next-app/templates/types.ts
Arindam200 Jul 26, 2024
a0742b5
Update packages/create-next-app/templates/types.ts
Arindam200 Jul 26, 2024
d837d4d
Update packages/create-next-app/create-app.ts
Arindam200 Jul 26, 2024
f03fde4
Merge branch 'vercel:canary' into feat-headless-api
Arindam200 Jul 27, 2024
8eaecc1
rename api to app-api
Arindam200 Jul 27, 2024
5af9353
Merge branch 'vercel:canary' into feat-headless-api
Arindam200 Jul 28, 2024
e2e63a1
Improve next.config.ts and next.config.mjs for API template
Tim-Zj Jul 29, 2024
0193596
Add route.js for API routes using App Router
Tim-Zj Jul 29, 2024
7cd19b1
Merge branch 'vercel:canary' into improve-api-template
Tim-Zj Jul 29, 2024
8f3a438
Save current changes
Tim-Zj Jul 29, 2024
30d146e
added route.js and route.ts
Tim-Zj Jul 30, 2024
945454a
fix: kebab is delicious
devjiwonchoi Jul 30, 2024
e9a4949
fix kebab
devjiwonchoi Jul 30, 2024
d149150
template name from api to app-api
devjiwonchoi Jul 30, 2024
7051cae
remove unnecessary added husky
devjiwonchoi Jul 30, 2024
03767ee
Update packages/create-next-app/index.ts
devjiwonchoi Jul 30, 2024
9af9575
remove eslint
devjiwonchoi Jul 30, 2024
fa604fb
unnecessary since is pages router config
devjiwonchoi Jul 30, 2024
9160ea4
add params to ts example
ephraimduncan Jul 30, 2024
6508b38
Merge branch 'canary' into feat-headless-api
devjiwonchoi Jul 31, 2024
7e83093
Merge branch 'vercel:canary' into feat-headless-api
Arindam200 Jul 31, 2024
557c201
add: status codes
Arindam200 Aug 1, 2024
016d3e3
Merge branch 'canary' into feat-headless-api
devjiwonchoi Aug 2, 2024
fc7f524
fix: left out merges
devjiwonchoi Aug 2, 2024
149a01c
Merge branch 'canary' into feat-headless-api
Arindam200 Aug 2, 2024
c864f01
Merge branch 'canary' into feat-headless-api
devjiwonchoi Aug 2, 2024
8698458
fix: left out merges
devjiwonchoi Aug 2, 2024
a150dfc
remove unnecessary api/
devjiwonchoi Aug 2, 2024
1639ef5
fix: specification
devjiwonchoi Aug 2, 2024
03aede3
remove unnecessary check
devjiwonchoi Aug 2, 2024
6fb01e7
Add user-info API route example with NextRequest and NextResponse
Tim-Zj Aug 3, 2024
b7c3966
Merge branch 'vercel:canary' into feat-headless-api
Tim-Zj Aug 3, 2024
325f55e
Add 404 and 500 error handling examples for API routes and update README
Tim-Zj Aug 3, 2024
a445aa5
Add 404 and 500 error handling examples for API routes and update README
Tim-Zj Aug 3, 2024
31095db
Add cors.ts
Tim-Zj Aug 3, 2024
894d544
add app-api for ts
devjiwonchoi Aug 15, 2024
93ea629
readme
devjiwonchoi Aug 15, 2024
5752374
remove js for now
devjiwonchoi Aug 15, 2024
d290b26
js
devjiwonchoi Aug 15, 2024
6dfb7d5
update id to number
devjiwonchoi Aug 15, 2024
4b45991
update
devjiwonchoi Aug 15, 2024
906539c
add test
devjiwonchoi Aug 15, 2024
1db9168
Merge branch 'canary' into feat-headless-api
Arindam200 Aug 16, 2024
db6c611
remove NextRequest type
Arindam200 Aug 16, 2024
d6a48aa
add `type` NextRequest
Arindam200 Aug 16, 2024
51f1102
Merge branch 'canary' into feat-headless-api
Arindam200 Aug 16, 2024
60a1272
Merge branch 'canary' into feat-headless-api
devjiwonchoi Aug 21, 2024
e7ac0a4
test: remove git diff
devjiwonchoi Aug 21, 2024
a32bb60
cannot delete `@types/react` yet
devjiwonchoi Aug 21, 2024
ef2c545
Revert "test: remove git diff"
devjiwonchoi Aug 21, 2024
cc203c2
Merge branch 'canary' into feat-headless-api
Arindam200 Aug 29, 2024
60d1651
Merge branch 'canary' into feat-headless-api
Arindam200 Sep 7, 2024
40e2578
Merge branch 'canary' into feat-headless-api
devjiwonchoi Sep 12, 2024
2e63b3f
move to example
devjiwonchoi Sep 12, 2024
80adef5
specification
devjiwonchoi Sep 12, 2024
10b88b8
util update
devjiwonchoi Sep 12, 2024
318067b
js hello world
devjiwonchoi Sep 12, 2024
4935b2f
write tests for example app-api
devjiwonchoi Sep 12, 2024
61603de
Update examples/app-api/package.json
devjiwonchoi Sep 12, 2024
ba031ae
remove not-found
devjiwonchoi Sep 12, 2024
57fb6a7
next latest
devjiwonchoi Sep 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/create-next-app/create-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export async function createApp({
skipInstall,
empty,
turbo,
api,
}: {
appPath: string
packageManager: PackageManager
Expand All @@ -51,6 +52,7 @@ export async function createApp({
skipInstall: boolean
empty: boolean
turbo: boolean
api?: boolean
}): Promise<void> {
let repoInfo: RepoInfo | undefined
const mode: TemplateMode = typescript ? 'ts' : 'js'
Expand Down Expand Up @@ -222,7 +224,7 @@ export async function createApp({
await installTemplate({
appName,
root,
template,
template: api ? 'app-api' : template,
mode,
packageManager,
isOnline,
Expand Down
8 changes: 5 additions & 3 deletions packages/create-next-app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const program = new Command(packageJson.name)
'--import-alias <prefix/*>',
'Specify import alias to use (default "@/*").'
)
.option('--api', 'Initialize a headless API using the App Router.')
.option('--empty', 'Initialize an empty project.')
.option(
'--use-npm',
Expand Down Expand Up @@ -273,7 +274,7 @@ async function run(): Promise<void> {
}
}

if (!opts.eslint && !args.includes('--no-eslint')) {
if (!opts.eslint && !args.includes('--no-eslint') && !opts.api) {
if (skipPrompt) {
opts.eslint = getPrefOrDefault('eslint')
} else {
Expand All @@ -292,7 +293,7 @@ async function run(): Promise<void> {
}
}

if (!opts.tailwind && !args.includes('--no-tailwind')) {
if (!opts.tailwind && !args.includes('--no-tailwind') && !opts.api) {
if (skipPrompt) {
opts.tailwind = getPrefOrDefault('tailwind')
} else {
Expand Down Expand Up @@ -330,7 +331,7 @@ async function run(): Promise<void> {
}
}

if (!opts.app && !args.includes('--no-app')) {
if (!opts.app && !args.includes('--no-app') && !opts.api) {
if (skipPrompt) {
opts.app = getPrefOrDefault('app')
} else {
Expand Down Expand Up @@ -428,6 +429,7 @@ async function run(): Promise<void> {
skipInstall: opts.skipInstall,
empty: opts.empty,
turbo: opts.turbo,
api: opts.api,
})
} catch (reason) {
if (!(reason instanceof DownloadError)) {
Expand Down
3 changes: 3 additions & 0 deletions packages/create-next-app/templates/app-api/js/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Rename this file to `.env.local` to use environment variables locally with `next dev`
# https://nextjs.org/docs/app/building-your-application/configuring/environment-variables
MY_HOST="example.com"
79 changes: 79 additions & 0 deletions packages/create-next-app/templates/app-api/js/README-template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/create-next-app).

## Getting Started

First, run the development server:

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `app/route.js`. The page auto-updates as you edit the file.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!

## Deploy on Vercel

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

## API Routes

This directory contains example API routes for the headless API app.

Here's an overview of the available routes:

1. Root Route (`/`):

- Provides a list of available API endpoints.
- Useful for discovering what the API offers.

2. Pokemon Routes:

- List all Pokemon (`/pokemon`): Get an overview of available Pokemon.
- Filter Pokemon by type (`/pokemon?type=grass`): Find Pokemon of a specific element.
- Get a specific Pokemon (`/pokemon/25`): Fetch details about a particular Pokemon by ID.

3. HTTP Methods Route (`/methods`):

- Information about supported HTTP methods.
- Great for learning about various types of API requests.

4. Specific HTTP Method Routes (`/methods/[method]`):

- Demonstrates different HTTP methods (GET, POST, PUT, DELETE, PATCH).
- Helps understand how different types of API requests behave.

5. Not Found Route:
- Handles requests to undefined routes.
- Returns a "Not Found" response for non-existent endpoints.

Each route is wrapped with middleware that adds:

- Request logging for monitoring and debugging.
- Origin checking for security.
- Authentication placeholders for future expansion.

To explore these routes:

1. Start the development server.
2. Open your browser and navigate to `http://localhost:3000/[route]`.
3. Examine the responses to understand how each route works.

For more details on the implementation, check the corresponding files in the `app` directory.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export async function GET() {
return new Response("Not Found", { status: 404 });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { NextResponse } from "next/server";
import { methods } from "../methods";
import { withMiddleware } from "@/app/with-middleware";

export const GET = withMiddleware(async (request, context) => {
const method = context.params.method.toUpperCase();
if (!methods.includes(method)) {
return NextResponse.json(
{ error: `Method ${method} is not supported.` },
{ status: 404 },
);
}

if (method === "GET") {
return NextResponse.json({ method: "GET" });
}

const url = request.nextUrl.toString();
const res = await fetch(url, { method });

// if method is HEAD, body is null
if (method === "HEAD") {
const { ok, statusText, status, body } = res;
return NextResponse.json({
ok,
statusText,
status,
body,
});
}

const data = await res.json();
return NextResponse.json(data);
});

export async function POST() {
return NextResponse.json({ method: "POST" });
}

export async function PUT() {
return NextResponse.json({ method: "PUT" });
}

export async function DELETE() {
return NextResponse.json({ method: "DELETE" });
}

export async function PATCH() {
return NextResponse.json({ method: "PATCH" });
}

export async function OPTIONS() {
return NextResponse.json({ method: "OPTIONS" });
}

export async function HEAD() {
return new Response(null, { status: 200 });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const methods = [
"GET",
"POST",
"PUT",
"DELETE",
"PATCH",
"OPTIONS",
"HEAD",
];
11 changes: 11 additions & 0 deletions packages/create-next-app/templates/app-api/js/app/methods/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { NextResponse } from "next/server";
import { methods } from "./methods";

export async function GET(request) {
const url = request.nextUrl.toString();
const apis = methods.map((method) => {
return { path: `${url}/${method}`, description: `${method} method` };
});

return NextResponse.json(apis);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { NextResponse } from "next/server";
import { withMiddleware } from "@/app/with-middleware";

export const GET = withMiddleware(async (_request, context) => {
const res = await fetch(
`https://api.vercel.app/pokemon/${context.params.number}`,
);
const pokemon = await res.json();

return NextResponse.json(pokemon);
});
51 changes: 51 additions & 0 deletions packages/create-next-app/templates/app-api/js/app/pokemon/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { NextResponse } from "next/server";
import { withMiddleware } from "@/app/with-middleware";

export const GET = withMiddleware(async (request) => {
const url = request.nextUrl.toString();

const { searchParams } = request.nextUrl;
const type = searchParams.get("type");

if (type && !POKEMON_TYPES.includes(type)) {
return NextResponse.json(
{ error: `Invalid type: '${type}'`, types: POKEMON_TYPES },
{ status: 400 },
);
}

const res = await fetch(
`https://api.vercel.app/pokemon${type ? `?type=${type}` : ""}`,
);
const data = await res.json();

const pokedex = data.map((pokemon) => {
return {
...pokemon,
url: `${url}/${pokemon.id}`,
};
});

return NextResponse.json(pokedex);
});

const POKEMON_TYPES = [
"normal",
"fire",
"water",
"electric",
"grass",
"ice",
"fighting",
"poison",
"ground",
"flying",
"psychic",
"bug",
"rock",
"ghost",
"dragon",
"dark",
"steel",
"fairy",
];
23 changes: 23 additions & 0 deletions packages/create-next-app/templates/app-api/js/app/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { NextResponse } from "next/server";

const paths = [
{ path: "pokemon", description: "gotta catch 'em all!" },
{ path: "pokemon?type=grass", description: "sort pokemon by grass type" },
{ path: "pokemon/25", description: "who's that pokemon?" },
{ path: "methods", description: "list of methods" },
];

export async function GET(request) {
const url = request.nextUrl.toString();
const apis = paths.map(({ path, description }) => {
return { path: url + path, description };
});

return NextResponse.json(apis, {
headers: {
// CORS: https://nextjs.org/docs/app/building-your-application/routing/route-handlers#cors
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET",
},
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { NextResponse } from "next/server";

export function withMiddleware(handler) {
return async (req, ctx) => {
try {
console.log(`Middleware: Request to ${req.method} ${req.url}`);

// Check if the request is coming from an allowed origin
if (!isAllowedOrigin(req)) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}

// Placeholder for authentication logic
const isAuthenticated = await checkAuth(req);
if (!isAuthenticated) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

// Call the original handler
const result = await handler(req, ctx);

// Log after executing the handler
console.log(`Middleware: Completed ${req.method} ${req.url}`);

return result;
} catch (error) {
console.error(
`Middleware: Error in route handler: ${req.method} ${req.url}`,
error,
);
return NextResponse.json(
{ error: "Internal Server Error" },
{ status: 500 },
);
}
};
}

function isAllowedOrigin(req) {
const referer = req.headers.get("referer");
const host = req.headers.get("host");

// Allow requests from the same origin
if (referer && host) {
const refererUrl = new URL(referer);
return refererUrl.host === host;
}

if (process.env.NODE_ENV === "development" && host?.includes("localhost")) {
return true;
}

return false;
}

async function checkAuth(req) {
// Placeholder authentication logic
// You can implement your actual auth check here
const authToken = req.headers.get("authorization");
return true; // For now, always return true
}
Loading
Loading