Building Your First API
Creating your own API endpoint

Pixl presents
Enough using other people's APIs. Build your own, with GET and POST endpoints that don't fall over on bad input.Enough using other people's APIs. Build your own, with GET and POST endpoints that don't fall over on bad input.

You've learned how to use APIs. Now let's build one. Making an API is WAY less complicated than it sounds. In this lesson you'll build a simple API that takes requests and sends back responses.
For some perspective, I've put 1,200+ hours and $2,000+ in AI tokens into building the four Seedly products (CRM, Sites, Communities and Dispatch). Big apps get built one small piece at a time, and today's piece is tiny.
What We're Building#
We'll make a simple API for a task list. Here's what it'll do.
- Return a list of tasks
- Return a single task by ID
- Create a new task
It's a small example, but it teaches the core ideas that show up in every API.
Setting Up#
For this example we'll use Next.js API routes. If you're on a different framework, the ideas are the same even if the code looks a lil different.
In Next.js, you make an API endpoint by adding a file in the app/api folder. The file path becomes the URL path.
app/api/tasks/route.js -> /api/tasksCreating a GET Endpoint#
Let's start with the simplest API there is, one that returns a list of tasks.
// app/api/tasks/route.js
// Our fake database (in real apps, you'd use a real database)
const tasks = [
{ id: 1, title: "Learn APIs", completed: false },
{ id: 2, title: "Build a project", completed: false },
{ id: 3, title: "Deploy to production", completed: false }
];
export async function GET() {
return Response.json(tasks);
}Yep, that's the whole thing. When someone sends a GET request to /api/tasks, they get the list of tasks back as JSON.
Testing Your Endpoint#
You can test your API a few different ways.
In your browser.
Just visit http://localhost:3000/api/tasks. You'll see the JSON.
With curl in the terminal.
curl http://localhost:3000/api/tasksWith a tool like Postman. Make a GET request to your URL and send it.
Getting a Single Task#
What if someone only wants one task? Then we need a dynamic route. In Next.js, you put brackets in the folder name.
app/api/tasks/[id]/route.js -> /api/tasks/1, /api/tasks/2, etc.// app/api/tasks/[id]/route.js
const tasks = [
{ id: 1, title: "Learn APIs", completed: false },
{ id: 2, title: "Build a project", completed: false },
{ id: 3, title: "Deploy to production", completed: false }
];
export async function GET(request, { params }) {
const { id } = await params;
const task = tasks.find(t => t.id === parseInt(id));
if (!task) {
return Response.json(
{ error: "Task not found" },
{ status: 404 }
);
}
return Response.json(task);
}Now here's what happens.
GET /api/tasks/1returns the first taskGET /api/tasks/99returns a 404 error
Step 1. Get the ID from the URL#
await params gives us the ID from the URL. In Next.js 15 and newer, params is a Promise, so you have to await it before you can read anything off it (forget the await and id comes back undefined). The ID shows up as a string, so parseInt turns it into a number before we compare.
Step 2. Find the task#
We search the tasks array for a matching ID.
Step 3. Handle not found#
If nothing matches, we send back a 404 error with a message.
Step 4. Return the task#
If we find it, we send the task back as JSON.
Creating a POST Endpoint#
Now let's handle making new tasks. POST requests carry data in the body.
// app/api/tasks/route.js
let tasks = [
{ id: 1, title: "Learn APIs", completed: false },
{ id: 2, title: "Build a project", completed: false },
{ id: 3, title: "Deploy to production", completed: false }
];
export async function GET() {
return Response.json(tasks);
}
export async function POST(request) {
// Get the data from the request body
const body = await request.json();
// Validate the data
if (!body.title) {
return Response.json(
{ error: "Title is required" },
{ status: 400 }
);
}
// Create the new task (next ID = highest ID so far + 1)
const newTask = {
id: Math.max(0, ...tasks.map(t => t.id)) + 1,
title: body.title,
completed: false
};
// Add it to our list
tasks.push(newTask);
// Return the new task with 201 Created status
return Response.json(newTask, { status: 201 });
}Now you can create tasks.
curl -X POST http://localhost:3000/api/tasks \
-H "Content-Type: application/json" \
-d '{"title": "My new task"}'The Complete API#
Here's the full task API with GET, POST, single-task lookup and DELETE. There's one catch when you split routes across two files. Both files need to see the SAME list of tasks. So we pull the list out into its own little file and have both routes import it.
// app/api/tasks/data.js
// Our fake database. It lives in memory, so it resets every time the
// server restarts. In real apps, you'd use a real database.
export const tasks = [
{ id: 1, title: "Learn APIs", completed: false },
{ id: 2, title: "Build a project", completed: false }
];// app/api/tasks/route.js
import { tasks } from "./data.js";
// GET all tasks
export async function GET() {
return Response.json(tasks);
}
// POST new task
export async function POST(request) {
const body = await request.json();
if (!body.title) {
return Response.json(
{ error: "Title is required" },
{ status: 400 }
);
}
const newTask = {
id: Math.max(0, ...tasks.map(t => t.id)) + 1,
title: body.title,
completed: false
};
tasks.push(newTask);
return Response.json(newTask, { status: 201 });
}// app/api/tasks/[id]/route.js
import { tasks } from "../data.js";
// GET single task
export async function GET(request, { params }) {
const { id } = await params;
const task = tasks.find(t => t.id === parseInt(id));
if (!task) {
return Response.json(
{ error: "Task not found" },
{ status: 404 }
);
}
return Response.json(task);
}
// DELETE task
export async function DELETE(request, { params }) {
const { id } = await params;
const index = tasks.findIndex(t => t.id === parseInt(id));
if (index === -1) {
return Response.json(
{ error: "Task not found" },
{ status: 404 }
);
}
tasks.splice(index, 1);
return new Response(null, { status: 204 });
}Best Practices#
Use the Right Status Codes#
| Action | Success Code |
|---|---|
| GET (found) | 200 OK |
| POST (created) | 201 Created |
| PUT/PATCH (updated) | 200 OK |
| DELETE (deleted) | 204 No Content |
Return Useful Errors#
A bare "Error." helps nobody. Tell the user what actually went wrong.
// Bad
return Response.json({ error: "Error" }, { status: 400 });
// Good
return Response.json(
{ error: "Title is required and must be at least 3 characters" },
{ status: 400 }
);Validate Everything#
Check every bit of input data. Don't assume any of it is right.
if (!body.title) {
return Response.json({ error: "Title is required" }, { status: 400 });
}
if (body.title.length < 3) {
return Response.json({ error: "Title must be at least 3 characters" }, { status: 400 });
}
if (body.title.length > 100) {
return Response.json({ error: "Title must be less than 100 characters" }, { status: 400 });
}What You Built#
Your API now handles these operations.
| Method | Endpoint | What It Does |
|---|---|---|
| GET | /api/tasks | Get all tasks |
| POST | /api/tasks | Create a task |
| GET | /api/tasks/1 | Get task 1 |
| DELETE | /api/tasks/1 | Delete task 1 |
That's a REAL API. It's simple, but it follows the same patterns the pros use.
TL;DR#
- APIs are just functions that handle HTTP requests
- Each HTTP method (GET, POST, PUT, DELETE) gets its own function
- Use the right status codes, like 200, 201, 400 and 404
- Always validate input data
- Send back helpful error messages
- In Next.js, the file path becomes the URL path
Almost There#

You've covered the core of the API Fundamentals module. Here's everything you've picked up.
- What APIs are and why they exist
- HTTP methods (GET, POST, PUT, DELETE)
- URLs and endpoints
- JSON data format
- Status codes
- Authentication with API keys and tokens
- Making API calls with fetch()
- Receiving webhooks
- Building your own API
- Making smart API design decisions
These are core skills for building anything modern. Almost every app you build will use APIs, provide APIs, or both, and you're ready to work with them in real projects now. In the last lesson of this module, you'll check out two real APIs you're likely to run into...
This lesson ends with a short activity.
