Securing and Observing Your Task Manager API with Codex

Welcome: Locking the Door and Turning on the Lights

Welcome back. At this point, your Task Manager API can:

  • Represent tasks with a shared Task model and in-memory store
  • Manipulate them through a clean service layer
  • Validate incoming payloads and surface clear errors through HTTP

That’s a solid backend—but right now it’s wide open to anyone who can hit your endpoints, and you have very little visibility into who’s calling what.

In this lesson, you’ll use Codex to:

  • Configure a secret API key in environment variables
  • Add a middleware that enforces that key on /api/tasks and logs every request
  • Build a Task Manager API testing panel in src/app/page.tsx that sends the right headers and lets you exercise all CRUD endpoints from the browser

By the end, you’ll have a Task Manager backend with a simple but real security gate and a handy UI “cockpit” for manual testing.

What We’re Building and Why It Matters

This lesson introduces two important backend concepts:

  • Configuration via environment variables

    • Secrets (like API keys) never belong in source code.
    • Using .env.local lets you swap configuration per environment without changing code.
  • Cross-cutting middleware for security and logging

    • Instead of securing each route individually, you add a single gate that runs before all /api/tasks handlers.
    • The same middleware can also log the who/what/when of every request.

On top of that, you’ll wire a simple UI panel that:

  • Lets you type an API key
  • Sends that key in the x-api-key header on all requests
  • Provides controls to call every task endpoint
  • Shows a live log of responses and client-side errors

This combination gives you both a lock on the API and a dashboard to test it.

How We’ll Use Codex in This Lesson

Codex will help you in three small, focused bursts:

  • Environment setup

    • Edit .env.local to define SECRET_API_KEY=...
  • Middleware implementation

    • Fill in src/middleware.ts with:
      • Path matching for /api/tasks
      • API key checks
      • Structured console logging
  • UI testing panel

    • Extend src/app/page.tsx into a control panel that:
      • Manages state for API key and task fields
      • Sends x-api-key in every fetch
      • Logs results in a “Logs” section

Your prompts should continue to follow the pattern you’ve been practicing:

  • “Modify only <file>.”
  • Describe the exact behavior in detail.
  • Explain any important security or logging rules.
  • “Do not modify any other files. Show the full updated contents of <file>.”
# .env.local

# TODO: Ask Codex to define a secret API key for protecting /api/tasks.
#
# Guidance example:
# "Codex, modify only .env.local. Add SECRET_API_KEY=my-secret-task-api-key
# so we can use it in middleware to secure our task routes."

# SECRET_API_KEY=TODO_REPLACE_ME

Step 1: Configuring a Secret API Key in .env.local

First, you’ll define the secret your middleware will use. The project already includes a placeholder .env.local file with guidance (as shown above).

Your job (with Codex’s help) is to turn this into a real configuration entry, for example:

  • SECRET_API_KEY=my-secret-task-api-key

Key points:

  • .env.local lives at the project root.
  • It should not be checked into public version control in real-world projects.
  • Your code accesses this value through process.env.SECRET_API_KEY, never by hard-coding the key.

A typical Codex prompt here is very small and very strict:

Codex, modify only .env.local.

Add a line defining SECRET_API_KEY with a non-empty placeholder value (for example, my-secret-task-api-key) that will be used to protect /api/tasks.

Do not modify any other files. Show the full updated .env.local file.

Once this is set, your middleware can start enforcing it.

// src/middleware.ts
// TODO: Ask Codex to implement security and logging middleware for /api/tasks.
//
// Guidance example:
// "Codex, modify only src/middleware.ts. For any path under /api/tasks,
// check x-api-key against process.env.SECRET_API_KEY and log details about
// the request. Return 401 JSON when unauthorized."

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const path = request.nextUrl.pathname;

  // TODO: If path starts with /api/tasks, enforce the x-api-key check here.
  // - Read the header.
  // - Compare to process.env.SECRET_API_KEY.
  // - Return 401 JSON if missing/invalid.

  // TODO: Log metadata about the request (timestamp, method, path, user-agent, ip).
  // Use console.log with a structured object so logs are easy to scan.

  return NextResponse.next();
}

export const config = {
  matcher: '/api/:path*',
};

Step 2: Protecting /api/tasks with Middleware and Logging Every Request

Next, you’ll implement security and logging logic in src/middleware.ts. The starter file already sketches the structure (as shown in the snippet).

You’ll ask Codex to complete this so that the middleware:

  • Runs for all /api/* routes

    • The config.matcher is already set to '/api/:path*'.
  • Enforces the API key on /api/tasks routes

    • Reads the header:
      • const apiKey = request.headers.get('x-api-key');
    • Compares it to process.env.SECRET_API_KEY.
    • If missing or incorrect, returns a 401 JSON response, for example:
      • return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  • Logs each request with structured metadata

    • For example:
      • timestamp: new Date().toISOString()
      • method: request.method
      • path
      • userAgent: request.headers.get('user-agent') ?? 'unknown'
      • ip: request.headers.get('x-forwarded-for') ?? request.ip ?? 'unknown'
    • Then:
      • console.log(logEntry);
  • Forwards authorized requests

    • If the path is under /api/tasks and the key is valid, call NextResponse.next() to let the request reach the route handler.
    • For all other /api paths, still log but skip the key check.

That gives you a central place to both filter and observe API traffic without touching individual route files.

A focused Codex prompt might look like:

Codex, modify only src/middleware.ts.

For any request whose pathname starts with /api/tasks, read the x-api-key header and compare it to process.env.SECRET_API_KEY. If the header is missing or does not match, return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) and do not call NextResponse.next().

For all /api/* routes (including /api/tasks), log a structured object with timestamp, method, path, userAgent, and ip using console.log. Use new Date().toISOString() for the timestamp and fall back to 'unknown' when needed.

When the request is authorized (or the path is not under /api/tasks), return NextResponse.next() to continue the chain.

Keep the existing config.matcher and do not modify any other files. Show the full updated contents of src/middleware.ts.

// src/app/page.tsx
// TODO: Ask Codex to turn this into a full Task Manager API testing panel
// that sends x-api-key and allows calling all CRUD endpoints.
//
// Guidance example:
// "Codex, modify only src/app/page.tsx. Build a simple control panel UI with
// inputs for API key, task id/title/content/dueDate, and a JSON text area for
// PUT/PATCH payloads, plus buttons that call each task endpoint and log results."

'use client';

import { useState } from 'react';

export default function Home() {
  const [logs, setLogs] = useState<string[]>([]);

  const addLog = (message: string) => {
    setLogs((prev) => [message, ...prev]);
    console.log(message);
  };

  // TODO: Ask Codex to add state for apiKey, taskId, title, content, dueDate,
  // and JSON update payload, plus handlers for each HTTP method that send
  // x-api-key and log the responses.

  return (
    <main style={{ padding: '2rem', fontFamily: 'Arial, sans-serif' }}>
      <h1>Task Manager API Demo (TODO: wire with Codex)</h1>

      {/* TODO: Add inputs and buttons for interacting with /api/tasks endpoints. */}

      <section style={{ marginTop: '1.5rem' }}>
        <h2>Logs</h2>
        <ul>
          {logs.map((log, idx) => (
            <li key={idx} style={{ whiteSpace: 'pre-wrap' }}>
              {log}
            </li>
          ))}
        </ul>
      </section>
    </main>
  );
}

Step 3: Building a Task Manager API Testing Panel with API Key Support

Now that /api/tasks is locked behind an API key, hitting it from the browser becomes more interesting. Instead of reaching for an external client, you’ll build a tiny testing panel inside src/app/page.tsx.

The starter file already includes a basic structure (shown above). You’ll ask Codex to evolve this page into a simple “mini Postman” that can:

  • Manage state for:

    • apiKey
    • taskId
    • title
    • content
    • dueDate
    • A JSON string for PUT/PATCH payloads
  • Render inputs and buttons for:

    • GET /api/tasks
    • GET /api/tasks/{id}
    • POST /api/tasks
    • PUT /api/tasks/{id}
    • PATCH /api/tasks/{id}
    • DELETE /api/tasks/{id}
  • Include the x-api-key header in every fetch, for example:

    • const res = await fetch('/api/tasks', { method: 'GET', headers: { 'x-api-key': apiKey } });
  • Append each request/response (or client-side error) to logs via addLog, ideally formatting JSON responses with JSON.stringify(data, null, 2) so they’re readable.

Along the way, you’ll also ask Codex to:

  • Guard against obvious client errors (e.g., “no ID provided” or “invalid JSON in update payload”).
  • Keep the UI simple and text-based, focusing on learning the backend, not styling.

This page will be your go-to tool for manually testing the secured CRUD API you’ve been building across the course.

A representative Codex prompt:

Codex, modify only src/app/page.tsx.

Turn this page into a simple Task Manager API testing panel that talks to /api/tasks and /api/tasks/[id]. Add React state for apiKey, taskId, title, content, dueDate, and a JSON updatePayload string used for PUT/PATCH.

Render inputs for all of these fields and buttons for:

  • GET /api/tasks
  • GET /api/tasks/{id}
  • POST /api/tasks
  • PUT /api/tasks/{id}
  • PATCH /api/tasks/{id}
  • DELETE /api/tasks/{id}

Each button should call the appropriate endpoint using fetch, always including the x-api-key header from the current apiKey state. For PUT/PATCH, parse updatePayload as JSON (log a clear error if parsing fails). For POST, send title, content, and optional dueDate as JSON.

Use addLog to prepend a formatted string describing each request and its result. When the response is JSON, pretty-print it with JSON.stringify(data, null, 2).

Keep the layout simple (basic HTML elements and inline styles are fine) and keep the existing Logs section intact, just wiring it to your new behavior. Do not modify any other files. Show the full updated contents of src/app/page.tsx.

Summary and What’s Next

In this lesson, you:

  • Defined a secret API key in .env.local so sensitive configuration lives outside your code.
  • Implemented security + logging middleware in src/middleware.ts that:
    • Protects /api/tasks with an x-api-key check
    • Logs structured metadata for every API request
  • Turned src/app/page.tsx into a Task Manager API testing panel that:
    • Sends the API key in headers
    • Exercises all CRUD routes
    • Shows logs for each interaction

You now have a Task Manager backend that’s not only structured and validated, but also gated and observable, plus a built-in UI to experiment with it.

From here, future courses can build on this foundation by adding more advanced concerns—like more sophisticated auth, better logging destinations, or persistent storage—while still using the same Codex-driven workflow you’ve been practicing.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal